mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] "Open in Discover" from dashboard (#127355)
This commit is contained in:
parent
96554163ad
commit
0ea741dcd5
16 changed files with 761 additions and 98 deletions
|
@ -57,7 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('dashboard panel full screen', async () => {
|
||||
const header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count');
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
|
||||
await testSubjects.click('embeddablePanelAction-togglePanel');
|
||||
await a11y.testAppSnapshot();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"requiredPlugins": ["uiActions", "embeddable", "discover"],
|
||||
"optionalPlugins": ["share", "usageCollection"],
|
||||
"configPath": ["xpack", "discoverEnhanced"],
|
||||
"requiredBundles": ["kibanaUtils", "data"],
|
||||
"requiredBundles": ["kibanaUtils", "data", "lens"],
|
||||
"owner": {
|
||||
"name": "Data Discovery",
|
||||
"githubTeam": "kibana-data-discovery"
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/
|
|||
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import { CoreStart } from '../../../../../../src/core/public';
|
||||
import { KibanaLocation } from '../../../../../../src/plugins/share/public';
|
||||
import { DOC_TYPE as LENS_DOC_TYPE } from '../../../../lens/common/constants';
|
||||
import * as shared from './shared';
|
||||
|
||||
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
||||
|
@ -41,6 +42,7 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I
|
|||
|
||||
public async isCompatible({ embeddable }: Context): Promise<boolean> {
|
||||
if (!embeddable) return false;
|
||||
if (embeddable.type === LENS_DOC_TYPE) return false;
|
||||
|
||||
const { core, plugins } = this.params.start();
|
||||
const { capabilities } = core.application;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
{ "path": "../../../src/plugins/discover/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/share/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../lens/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/url_forwarding/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
|
||||
|
|
|
@ -9,14 +9,12 @@ import { createMockDatasource } from '../mocks';
|
|||
import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { DatasourcePublicAPI } from '../types';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { Capabilities } from 'kibana/public';
|
||||
|
||||
describe('getLayerMetaInfo', () => {
|
||||
const capabilities = {
|
||||
navLinks: { discover: true },
|
||||
discover: { show: true },
|
||||
} as unknown as RecursiveReadonly<Capabilities>;
|
||||
};
|
||||
it('should return error in case of no data', () => {
|
||||
expect(
|
||||
getLayerMetaInfo(createMockDatasource('testDatasource'), {}, undefined, capabilities).error
|
||||
|
@ -85,7 +83,7 @@ describe('getLayerMetaInfo', () => {
|
|||
{
|
||||
navLinks: { discover: false },
|
||||
discover: { show: true },
|
||||
} as unknown as RecursiveReadonly<Capabilities>
|
||||
}
|
||||
).isVisible
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
|
@ -98,7 +96,7 @@ describe('getLayerMetaInfo', () => {
|
|||
{
|
||||
navLinks: { discover: true },
|
||||
discover: { show: false },
|
||||
} as unknown as RecursiveReadonly<Capabilities>
|
||||
}
|
||||
).isVisible
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -178,7 +176,7 @@ describe('combineQueryAndFilters', () => {
|
|||
undefined
|
||||
)
|
||||
).toEqual({
|
||||
query: { language: 'kuery', query: '( myfield: * ) AND ( otherField: * )' },
|
||||
query: { language: 'kuery', query: '( ( myfield: * ) AND ( otherField: * ) )' },
|
||||
filters: [],
|
||||
});
|
||||
});
|
||||
|
@ -198,7 +196,7 @@ describe('combineQueryAndFilters', () => {
|
|||
},
|
||||
undefined
|
||||
)
|
||||
).toEqual({ query: { language: 'kuery', query: '( otherField: * )' }, filters: [] });
|
||||
).toEqual({ query: { language: 'kuery', query: 'otherField: *' }, filters: [] });
|
||||
});
|
||||
|
||||
it('should build single kuery expression from meta filters and join using OR and AND at the right level', () => {
|
||||
|
@ -238,6 +236,7 @@ describe('combineQueryAndFilters', () => {
|
|||
filters: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should assign kuery meta filters to app filters if existing query is using lucene language', () => {
|
||||
expect(
|
||||
combineQueryAndFilters(
|
||||
|
@ -293,6 +292,7 @@ describe('combineQueryAndFilters', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should append lucene meta filters to app filters even if existing filters are using kuery', () => {
|
||||
expect(
|
||||
combineQueryAndFilters(
|
||||
|
@ -385,7 +385,7 @@ describe('combineQueryAndFilters', () => {
|
|||
must: [
|
||||
{
|
||||
query_string: {
|
||||
query: '( anotherField )',
|
||||
query: 'anotherField',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -407,6 +407,7 @@ describe('combineQueryAndFilters', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should append lucene meta filters to an existing lucene query', () => {
|
||||
expect(
|
||||
combineQueryAndFilters(
|
||||
|
@ -461,10 +462,158 @@ describe('combineQueryAndFilters', () => {
|
|||
],
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: '( myField ) AND ( anotherField )',
|
||||
query: '( ( myField ) AND ( anotherField ) )',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept multiple queries (and play nice with meta filters)', () => {
|
||||
const { query, filters } = combineQueryAndFilters(
|
||||
[
|
||||
{ language: 'lucene', query: 'myFirstField' },
|
||||
{ language: 'lucene', query: 'mySecondField' },
|
||||
{ language: 'kuery', query: 'myThirdField : *' },
|
||||
],
|
||||
[],
|
||||
{
|
||||
id: 'testDatasource',
|
||||
columns: [],
|
||||
filters: {
|
||||
enabled: {
|
||||
kuery: [[{ language: 'kuery', query: 'myFourthField : *' }]],
|
||||
lucene: [[{ language: 'lucene', query: 'myFifthField' }]],
|
||||
},
|
||||
disabled: { kuery: [], lucene: [] },
|
||||
},
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(query).toEqual({
|
||||
language: 'lucene',
|
||||
query: '( ( myFirstField ) AND ( mySecondField ) AND ( myFifthField ) )',
|
||||
});
|
||||
|
||||
expect(filters).toEqual([
|
||||
{
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: 'myThirdField',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: 'myFourthField',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
meta: {
|
||||
alias: 'Lens context (kuery)',
|
||||
disabled: false,
|
||||
index: 'testDatasource',
|
||||
negate: false,
|
||||
type: 'custom',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore all empty queries', () => {
|
||||
const emptyQueryAndFilters = {
|
||||
filters: [],
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
combineQueryAndFilters(
|
||||
[{ language: 'lucene', query: '' }],
|
||||
[],
|
||||
{
|
||||
id: 'testDatasource',
|
||||
columns: [],
|
||||
filters: {
|
||||
enabled: {
|
||||
kuery: [[{ language: 'kuery', query: '' }]],
|
||||
lucene: [],
|
||||
},
|
||||
disabled: { kuery: [], lucene: [] },
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
).toEqual(emptyQueryAndFilters);
|
||||
|
||||
expect(
|
||||
combineQueryAndFilters(
|
||||
{ language: 'lucene', query: '' },
|
||||
[],
|
||||
{
|
||||
id: 'testDatasource',
|
||||
columns: [],
|
||||
filters: {
|
||||
enabled: {
|
||||
kuery: [[{ language: 'kuery', query: '' }]],
|
||||
lucene: [],
|
||||
},
|
||||
disabled: { kuery: [], lucene: [] },
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
).toEqual(emptyQueryAndFilters);
|
||||
|
||||
expect(
|
||||
combineQueryAndFilters(
|
||||
undefined,
|
||||
[],
|
||||
{
|
||||
id: 'testDatasource',
|
||||
columns: [],
|
||||
filters: {
|
||||
enabled: {
|
||||
kuery: [[{ language: 'kuery', query: '' }]],
|
||||
lucene: [],
|
||||
},
|
||||
disabled: { kuery: [], lucene: [] },
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
).toEqual(emptyQueryAndFilters);
|
||||
});
|
||||
|
||||
it('should work for complex cases of nested meta filters', () => {
|
||||
// scenario overview:
|
||||
// A kuery query
|
||||
|
@ -596,7 +745,7 @@ describe('combineQueryAndFilters', () => {
|
|||
query: {
|
||||
language: 'kuery',
|
||||
query:
|
||||
'( myField: * ) AND ( ( bytes > 4000 ) AND ( ( memory > 5000 ) OR ( memory >= 15000 ) ) AND ( myField: * ) AND ( otherField >= 15 ) )',
|
||||
'( ( myField: * ) AND ( bytes > 4000 ) AND ( ( memory > 5000 ) OR ( memory >= 15000 ) ) AND ( myField: * ) AND ( otherField >= 15 ) )',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -796,7 +945,7 @@ describe('combineQueryAndFilters', () => {
|
|||
],
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: '( myField ) AND ( anotherField )',
|
||||
query: '( ( myField ) AND ( anotherField ) )',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { Capabilities } from 'kibana/public';
|
||||
import { partition } from 'lodash';
|
||||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import { Datasource } from '../types';
|
||||
|
||||
|
@ -24,10 +25,17 @@ export const getShowUnderlyingDataLabel = () =>
|
|||
defaultMessage: 'Open in Discover',
|
||||
});
|
||||
|
||||
function joinQueries(queries: Query[][] | undefined) {
|
||||
if (!queries) {
|
||||
return '';
|
||||
/**
|
||||
* Joins a series of queries.
|
||||
*
|
||||
* Uses "AND" along dimension 1 and "OR" along dimension 2
|
||||
*/
|
||||
function joinQueries(queries: Query[][]) {
|
||||
// leave a single query alone
|
||||
if (queries.length === 1 && queries[0].length === 1) {
|
||||
return queries[0][0].query;
|
||||
}
|
||||
|
||||
const expression = queries
|
||||
.filter((subQueries) => subQueries.length)
|
||||
.map((subQueries) =>
|
||||
|
@ -56,7 +64,10 @@ export function getLayerMetaInfo(
|
|||
currentDatasource: Datasource | undefined,
|
||||
datasourceState: unknown,
|
||||
activeData: TableInspectorAdapter | undefined,
|
||||
capabilities: RecursiveReadonly<Capabilities>
|
||||
capabilities: RecursiveReadonly<{
|
||||
navLinks: Capabilities['navLinks'];
|
||||
discover?: Capabilities['discover'];
|
||||
}>
|
||||
): { meta: LayerMetaInfo | undefined; isVisible: boolean; error: string | undefined } {
|
||||
const isVisible = Boolean(capabilities.navLinks?.discover && capabilities.discover?.show);
|
||||
// If Multiple tables, return
|
||||
|
@ -123,40 +134,65 @@ export function getLayerMetaInfo(
|
|||
}
|
||||
|
||||
// This enforces on assignment time that the two props are not the same
|
||||
type LanguageAssignments =
|
||||
| { queryLanguage: 'lucene'; filtersLanguage: 'kuery' }
|
||||
| { queryLanguage: 'kuery'; filtersLanguage: 'lucene' };
|
||||
type QueryLanguage = 'lucene' | 'kuery';
|
||||
|
||||
/**
|
||||
* Translates an arbitrarily-large set of @type {Query}s (including those supplied in @type {LayerMetaInfo})
|
||||
* and existing Kibana @type {Filter}s into a single query and a new set of @type {Filter}s. This allows them to
|
||||
* function as an equivalent context in Discover.
|
||||
*
|
||||
* If some of the queries are in KQL and some in Lucene, all the queries in one language will be merged into
|
||||
* a large query to be shown in the query bar, while the queries in the other language will be encoded as an
|
||||
* extra filter pill.
|
||||
*/
|
||||
export function combineQueryAndFilters(
|
||||
query: Query | undefined,
|
||||
query: Query | Query[] | undefined,
|
||||
filters: Filter[],
|
||||
meta: LayerMetaInfo,
|
||||
dataViews: DataViewBase[] | undefined
|
||||
) {
|
||||
// Unless a lucene query is already defined, kuery is assigned to it
|
||||
const { queryLanguage, filtersLanguage }: LanguageAssignments =
|
||||
query?.language === 'lucene'
|
||||
? { queryLanguage: 'lucene', filtersLanguage: 'kuery' }
|
||||
: { queryLanguage: 'kuery', filtersLanguage: 'lucene' };
|
||||
const queries: {
|
||||
kuery: Query[];
|
||||
lucene: Query[];
|
||||
} = {
|
||||
kuery: [],
|
||||
lucene: [],
|
||||
};
|
||||
|
||||
let newQuery = query;
|
||||
const enabledFilters = meta.filters.enabled;
|
||||
if (enabledFilters[queryLanguage]?.length) {
|
||||
const filtersQuery = joinQueries(enabledFilters[queryLanguage]);
|
||||
newQuery = {
|
||||
language: queryLanguage,
|
||||
query: query?.query.trim()
|
||||
? `( ${query.query} ) ${filtersQuery ? `AND ${filtersQuery}` : ''}`
|
||||
: filtersQuery,
|
||||
};
|
||||
}
|
||||
const allQueries = Array.isArray(query) ? query : query ? [query] : [];
|
||||
const nonEmptyQueries = allQueries.filter((q) => Boolean(q.query.trim()));
|
||||
|
||||
[queries.lucene, queries.kuery] = partition(nonEmptyQueries, (q) => q.language === 'lucene');
|
||||
|
||||
const queryLanguage: QueryLanguage =
|
||||
(nonEmptyQueries[0]?.language as QueryLanguage | undefined) || 'kuery';
|
||||
|
||||
const newQuery = {
|
||||
language: queryLanguage,
|
||||
query: joinQueries([
|
||||
...queries[queryLanguage].map((q) => [q]),
|
||||
...(meta.filters.enabled[queryLanguage] || []),
|
||||
]),
|
||||
};
|
||||
|
||||
const filtersLanguage = queryLanguage === 'lucene' ? 'kuery' : 'lucene';
|
||||
|
||||
// make a copy as the original filters are readonly
|
||||
const newFilters = [...filters];
|
||||
|
||||
const dataView = dataViews?.find(({ id }) => id === meta.id);
|
||||
if (enabledFilters[filtersLanguage]?.length) {
|
||||
const queryExpression = joinQueries(enabledFilters[filtersLanguage]);
|
||||
// Append the new filter based on the queryExpression to the existing ones
|
||||
|
||||
const hasQueriesInFiltersLanguage = Boolean(
|
||||
meta.filters.enabled[filtersLanguage]?.length || queries[filtersLanguage].length
|
||||
);
|
||||
|
||||
if (hasQueriesInFiltersLanguage) {
|
||||
const queryExpression = joinQueries([
|
||||
...queries[filtersLanguage].map((q) => [q]),
|
||||
...(meta.filters.enabled[filtersLanguage] || []),
|
||||
]);
|
||||
|
||||
// Create new filter to encode the rest of the query information
|
||||
newFilters.push(
|
||||
buildCustomFilter(
|
||||
meta.id!,
|
||||
|
@ -171,11 +207,11 @@ export function combineQueryAndFilters(
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
// for each disabled filter create a new custom filter and disable it
|
||||
// note that both languages go into the filter bar
|
||||
const disabledFilters = meta.filters.disabled;
|
||||
for (const language of ['lucene', 'kuery'] as const) {
|
||||
const [disabledQueries] = disabledFilters[language] || [];
|
||||
const [disabledQueries] = meta.filters.disabled[language] || [];
|
||||
for (const disabledQuery of disabledQueries || []) {
|
||||
let label = disabledQuery.query as string;
|
||||
if (language === 'lucene') {
|
||||
|
@ -193,5 +229,6 @@ export function combineQueryAndFilters(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { filters: newFilters, query: newQuery };
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ export class EditorFrameService {
|
|||
private readonly datasources: Array<Datasource | (() => Promise<Datasource>)> = [];
|
||||
private readonly visualizations: Array<Visualization | (() => Promise<Visualization>)> = [];
|
||||
|
||||
private loadDatasources = () => collectAsyncDefinitions(this.datasources);
|
||||
public loadDatasources = () => collectAsyncDefinitions(this.datasources);
|
||||
public loadVisualizations = () => collectAsyncDefinitions(this.visualizations);
|
||||
|
||||
/**
|
||||
|
|
|
@ -145,11 +145,14 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
inspector: inspectorPluginMock.createStartContract(),
|
||||
getTrigger,
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -189,9 +192,15 @@ describe('embeddable', () => {
|
|||
basePath,
|
||||
indexPatternService: {} as DataViewsContract,
|
||||
inspector: inspectorPluginMock.createStartContract(),
|
||||
capabilities: { canSaveDashboards: true, canSaveVisualizations: true },
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -236,9 +245,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -294,9 +306,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -341,9 +356,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -387,9 +405,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -429,9 +450,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -474,9 +498,15 @@ describe('embeddable', () => {
|
|||
basePath,
|
||||
inspector: inspectorPluginMock.createStartContract(),
|
||||
indexPatternService: {} as DataViewsContract,
|
||||
capabilities: { canSaveDashboards: true, canSaveVisualizations: true },
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -526,9 +556,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -576,9 +609,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -633,9 +669,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -659,7 +698,7 @@ describe('embeddable', () => {
|
|||
expect.objectContaining({
|
||||
timeRange,
|
||||
query: [query, savedVis.state.query],
|
||||
filters: mockInjectFilterReferences(filters, []),
|
||||
filters,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -691,9 +730,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -723,8 +765,10 @@ describe('embeddable', () => {
|
|||
|
||||
it('should merge external context with query and filters of the saved object', async () => {
|
||||
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
|
||||
const query: Query = { language: 'kquery', query: 'external filter' };
|
||||
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
|
||||
const query: Query = { language: 'kquery', query: 'external query' };
|
||||
const filters: Filter[] = [
|
||||
{ meta: { alias: 'external filter', negate: false, disabled: false } },
|
||||
];
|
||||
|
||||
const newSavedVis = {
|
||||
...savedVis,
|
||||
|
@ -750,9 +794,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -772,18 +819,16 @@ describe('embeddable', () => {
|
|||
await embeddable.initializeSavedVis(input);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({
|
||||
timeRange,
|
||||
query: [query, { language: 'kquery', query: 'saved filter' }],
|
||||
// actual index pattern id gets injected
|
||||
filters: mockInjectFilterReferences(
|
||||
[
|
||||
filters[0],
|
||||
{ meta: { alias: 'test', negate: false, disabled: false, index: 'injected!' } },
|
||||
],
|
||||
[]
|
||||
),
|
||||
});
|
||||
const expectedFilters = [
|
||||
...input.filters!,
|
||||
...mockInjectFilterReferences(newSavedVis.state.filters, []),
|
||||
];
|
||||
expect(expressionRenderer.mock.calls[0][0].searchContext?.timeRange).toEqual(timeRange);
|
||||
expect(expressionRenderer.mock.calls[0][0].searchContext?.filters).toEqual(expectedFilters);
|
||||
expect(expressionRenderer.mock.calls[0][0].searchContext?.query).toEqual([
|
||||
query,
|
||||
{ language: 'kquery', query: 'saved filter' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should execute trigger on event from expression renderer', async () => {
|
||||
|
@ -798,9 +843,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -846,9 +894,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -891,9 +942,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -951,9 +1005,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -1030,9 +1087,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -1084,9 +1144,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -1135,9 +1198,12 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
datasourceMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
|
@ -1207,6 +1273,8 @@ describe('embeddable', () => {
|
|||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
discover: {},
|
||||
navLinks: {},
|
||||
},
|
||||
getTrigger,
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
|
@ -1216,6 +1284,7 @@ describe('embeddable', () => {
|
|||
onEditAction: onEditActionMock,
|
||||
} as unknown as Visualization,
|
||||
},
|
||||
datasourceMap: {},
|
||||
documentToExpression: documentToExpressionMock,
|
||||
},
|
||||
{ id: '123' } as unknown as LensEmbeddableInput
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isEqual, uniqBy } from 'lodash';
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { DataViewBase, Filter } from '@kbn/es-query';
|
||||
import {
|
||||
ExecutionContextSearch,
|
||||
Query,
|
||||
|
@ -55,21 +55,25 @@ import {
|
|||
LensTableRowContextMenuEvent,
|
||||
VisualizationMap,
|
||||
Visualization,
|
||||
DatasourceMap,
|
||||
Datasource,
|
||||
} from '../types';
|
||||
|
||||
import type { DataViewsContract, DataView } from '../../../../../src/plugins/data_views/public';
|
||||
import { getEditPath, DOC_TYPE, PLUGIN_ID } from '../../common';
|
||||
import type {
|
||||
Capabilities,
|
||||
IBasePath,
|
||||
KibanaExecutionContext,
|
||||
ThemeServiceStart,
|
||||
} from '../../../../../src/core/public';
|
||||
import { LensAttributeService } from '../lens_attribute_service';
|
||||
import type { ErrorMessage } from '../editor_frame_service/types';
|
||||
import type { ErrorMessage, TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import { getLensInspectorService, LensInspector } from '../lens_inspector_service';
|
||||
import { SharingSavedObjectProps } from '../types';
|
||||
import type { SpacesPluginStart } from '../../../spaces/public';
|
||||
import { inferTimeField } from '../utils';
|
||||
import { getActiveDatasourceIdFromDoc, getIndexPatternsObjects, inferTimeField } from '../utils';
|
||||
import { getLayerMetaInfo, combineQueryAndFilters } from '../app_plugin/show_underlying_data';
|
||||
|
||||
export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>;
|
||||
|
||||
|
@ -114,6 +118,7 @@ export interface LensEmbeddableDeps {
|
|||
) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
indexPatternService: DataViewsContract;
|
||||
expressionRenderer: ReactExpressionRendererType;
|
||||
timefilter: TimefilterContract;
|
||||
|
@ -121,12 +126,25 @@ export interface LensEmbeddableDeps {
|
|||
inspector: InspectorStart;
|
||||
getTrigger?: UiActionsStart['getTrigger'] | undefined;
|
||||
getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions'];
|
||||
capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean };
|
||||
capabilities: {
|
||||
canSaveVisualizations: boolean;
|
||||
canSaveDashboards: boolean;
|
||||
navLinks: Capabilities['navLinks'];
|
||||
discover: Capabilities['discover'];
|
||||
};
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
spaces?: SpacesPluginStart;
|
||||
theme: ThemeServiceStart;
|
||||
}
|
||||
|
||||
export interface ViewUnderlyingDataArgs {
|
||||
indexPatternId: string;
|
||||
timeRange: TimeRange;
|
||||
filters: Filter[];
|
||||
query: Query | undefined;
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
const getExpressionFromDocument = async (
|
||||
document: Document,
|
||||
documentToExpression: LensEmbeddableDeps['documentToExpression']
|
||||
|
@ -138,6 +156,52 @@ const getExpressionFromDocument = async (
|
|||
};
|
||||
};
|
||||
|
||||
function getViewUnderlyingDataArgs({
|
||||
activeDatasource,
|
||||
activeDatasourceState,
|
||||
activeData,
|
||||
dataViews,
|
||||
capabilities,
|
||||
query,
|
||||
filters,
|
||||
timeRange,
|
||||
}: {
|
||||
activeDatasource: Datasource;
|
||||
activeDatasourceState: unknown;
|
||||
activeData: TableInspectorAdapter | undefined;
|
||||
dataViews: DataViewBase[] | undefined;
|
||||
capabilities: LensEmbeddableDeps['capabilities'];
|
||||
query: ExecutionContextSearch['query'];
|
||||
filters: Filter[];
|
||||
timeRange: TimeRange;
|
||||
}) {
|
||||
const { error, meta } = getLayerMetaInfo(
|
||||
activeDatasource,
|
||||
activeDatasourceState,
|
||||
activeData,
|
||||
capabilities
|
||||
);
|
||||
|
||||
if (error || !meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filters: newFilters, query: newQuery } = combineQueryAndFilters(
|
||||
query,
|
||||
filters,
|
||||
meta,
|
||||
dataViews
|
||||
);
|
||||
|
||||
return {
|
||||
indexPatternId: meta.id,
|
||||
timeRange,
|
||||
filters: newFilters,
|
||||
query: newQuery,
|
||||
columns: meta.columns,
|
||||
};
|
||||
}
|
||||
|
||||
export class Embeddable
|
||||
extends AbstractEmbeddable<LensEmbeddableInput, LensEmbeddableOutput>
|
||||
implements ReferenceOrValueEmbeddable<LensByValueInput, LensByReferenceInput>
|
||||
|
@ -173,6 +237,16 @@ export class Embeddable
|
|||
searchSessionId?: string;
|
||||
} = {};
|
||||
|
||||
private activeDataInfo: {
|
||||
activeData?: TableInspectorAdapter;
|
||||
activeDatasource?: Datasource;
|
||||
activeDatasourceState?: unknown;
|
||||
} = {};
|
||||
|
||||
private indexPatterns: DataView[] = [];
|
||||
|
||||
private viewUnderlyingDataArgs?: ViewUnderlyingDataArgs;
|
||||
|
||||
constructor(
|
||||
private deps: LensEmbeddableDeps,
|
||||
initialInput: LensEmbeddableInput,
|
||||
|
@ -387,7 +461,8 @@ export class Embeddable
|
|||
return isDirty;
|
||||
}
|
||||
|
||||
private updateActiveData: ExpressionWrapperProps['onData$'] = () => {
|
||||
private updateActiveData: ExpressionWrapperProps['onData$'] = (_, adapters) => {
|
||||
this.activeDataInfo.activeData = adapters?.tables?.tables;
|
||||
if (this.input.onLoad) {
|
||||
// once onData$ is get's called from expression renderer, loading becomes false
|
||||
this.input.onLoad(false);
|
||||
|
@ -495,22 +570,25 @@ export class Embeddable
|
|||
if (!this.savedVis) {
|
||||
throw new Error('savedVis is required for getMergedSearchContext');
|
||||
}
|
||||
const output: ExecutionContextSearch = {
|
||||
|
||||
const context: ExecutionContextSearch = {
|
||||
timeRange: this.externalSearchContext.timeRange,
|
||||
query: [this.savedVis.state.query],
|
||||
filters: this.deps.injectFilterReferences(
|
||||
this.savedVis.state.filters,
|
||||
this.savedVis.references
|
||||
),
|
||||
};
|
||||
|
||||
if (this.externalSearchContext.query) {
|
||||
output.query = [this.externalSearchContext.query, this.savedVis.state.query];
|
||||
} else {
|
||||
output.query = [this.savedVis.state.query];
|
||||
}
|
||||
if (this.externalSearchContext.filters?.length) {
|
||||
output.filters = [...this.externalSearchContext.filters, ...this.savedVis.state.filters];
|
||||
} else {
|
||||
output.filters = [...this.savedVis.state.filters];
|
||||
context.query = [this.externalSearchContext.query, ...(context.query as Query[])];
|
||||
}
|
||||
|
||||
output.filters = this.deps.injectFilterReferences(output.filters, this.savedVis.references);
|
||||
return output;
|
||||
if (this.externalSearchContext.filters?.length) {
|
||||
context.filters = [...this.externalSearchContext.filters, ...(context.filters as Filter[])];
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private get onEditAction(): Visualization['onEditAction'] {
|
||||
|
@ -597,28 +675,78 @@ export class Embeddable
|
|||
}
|
||||
}
|
||||
|
||||
private async loadViewUnderlyingDataArgs(): Promise<boolean> {
|
||||
const mergedSearchContext = this.getMergedSearchContext();
|
||||
|
||||
if (!this.activeDataInfo.activeData || !mergedSearchContext.timeRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeDatasourceId = getActiveDatasourceIdFromDoc(this.savedVis);
|
||||
if (!activeDatasourceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.activeDataInfo.activeDatasource = this.deps.datasourceMap[activeDatasourceId];
|
||||
const docDatasourceState = this.savedVis?.state.datasourceStates[activeDatasourceId];
|
||||
|
||||
if (!this.activeDataInfo.activeDatasourceState) {
|
||||
this.activeDataInfo.activeDatasourceState =
|
||||
await this.activeDataInfo.activeDatasource.initialize(
|
||||
docDatasourceState,
|
||||
this.savedVis?.references
|
||||
);
|
||||
}
|
||||
|
||||
const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({
|
||||
activeDatasource: this.activeDataInfo.activeDatasource,
|
||||
activeDatasourceState: this.activeDataInfo.activeDatasourceState,
|
||||
activeData: this.activeDataInfo.activeData,
|
||||
dataViews: this.indexPatterns,
|
||||
capabilities: this.deps.capabilities,
|
||||
query: mergedSearchContext.query,
|
||||
filters: mergedSearchContext.filters || [],
|
||||
timeRange: mergedSearchContext.timeRange,
|
||||
});
|
||||
|
||||
const loaded = typeof viewUnderlyingDataArgs !== 'undefined';
|
||||
if (loaded) {
|
||||
this.viewUnderlyingDataArgs = viewUnderlyingDataArgs;
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the necessary arguments to view the underlying data in discover.
|
||||
*
|
||||
* Only makes sense to call this after canViewUnderlyingData has been checked
|
||||
*/
|
||||
public getViewUnderlyingDataArgs() {
|
||||
return this.viewUnderlyingDataArgs;
|
||||
}
|
||||
|
||||
public canViewUnderlyingData() {
|
||||
return this.loadViewUnderlyingDataArgs();
|
||||
}
|
||||
|
||||
async initializeOutput() {
|
||||
if (!this.savedVis) {
|
||||
return;
|
||||
}
|
||||
const responses = await Promise.allSettled(
|
||||
uniqBy(
|
||||
this.savedVis.references.filter(({ type }) => type === 'index-pattern'),
|
||||
'id'
|
||||
).map(({ id }) => this.deps.indexPatternService.get(id))
|
||||
|
||||
const { indexPatterns } = await getIndexPatternsObjects(
|
||||
this.savedVis?.references.map(({ id }) => id) || [],
|
||||
this.deps.indexPatternService
|
||||
);
|
||||
const indexPatterns = responses
|
||||
.filter(
|
||||
(response): response is PromiseFulfilledResult<DataView> => response.status === 'fulfilled'
|
||||
)
|
||||
.map(({ value }) => value);
|
||||
|
||||
this.indexPatterns = uniqBy(indexPatterns, 'id');
|
||||
|
||||
// passing edit url and index patterns to the output of this embeddable for
|
||||
// the container to pick them up and use them to configure filter bar and
|
||||
// config dropdown correctly.
|
||||
const input = this.getInput();
|
||||
|
||||
this.errors = this.maybeAddTimeRangeError(this.errors, input, indexPatterns);
|
||||
this.errors = this.maybeAddTimeRangeError(this.errors, input, this.indexPatterns);
|
||||
|
||||
if (this.errors) {
|
||||
this.logError('validation');
|
||||
|
@ -633,7 +761,7 @@ export class Embeddable
|
|||
title,
|
||||
editPath: getEditPath(savedObjectId),
|
||||
editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`),
|
||||
indexPatterns,
|
||||
indexPatterns: this.indexPatterns,
|
||||
});
|
||||
|
||||
// deferred loading of this embeddable is complete
|
||||
|
|
|
@ -26,7 +26,7 @@ import { DOC_TYPE } from '../../common/constants';
|
|||
import { ErrorMessage } from '../editor_frame_service/types';
|
||||
import { extract, inject } from '../../common/embeddable_factory';
|
||||
import type { SpacesPluginStart } from '../../../spaces/public';
|
||||
import { VisualizationMap } from '../types';
|
||||
import { DatasourceMap, VisualizationMap } from '../types';
|
||||
|
||||
export interface LensEmbeddableStartServices {
|
||||
timefilter: TimefilterContract;
|
||||
|
@ -43,6 +43,7 @@ export interface LensEmbeddableStartServices {
|
|||
) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
spaces?: SpacesPluginStart;
|
||||
theme: ThemeServiceStart;
|
||||
}
|
||||
|
@ -92,6 +93,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
documentToExpression,
|
||||
injectFilterReferences,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
uiActions,
|
||||
coreHttp,
|
||||
attributeService,
|
||||
|
@ -118,9 +120,12 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
documentToExpression,
|
||||
injectFilterReferences,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
capabilities: {
|
||||
canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls),
|
||||
canSaveVisualizations: Boolean(capabilities.visualize.save),
|
||||
navLinks: capabilities.navLinks,
|
||||
discover: capabilities.discover,
|
||||
},
|
||||
usageCollection,
|
||||
theme,
|
||||
|
|
|
@ -16,8 +16,12 @@ import type {
|
|||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
} from '../../../../src/plugins/data/public';
|
||||
import {
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
EmbeddableSetup,
|
||||
EmbeddableStart,
|
||||
} from '../../../../src/plugins/embeddable/public';
|
||||
import type { DataViewsPublicPluginStart } from '../../../../src/plugins/data_views/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
import type { DashboardStart } from '../../../../src/plugins/dashboard/public';
|
||||
import type { SpacesPluginStart } from '../../spaces/public';
|
||||
import type {
|
||||
|
@ -80,6 +84,7 @@ import type {
|
|||
LensTopNavMenuEntryGenerator,
|
||||
} from './types';
|
||||
import { getLensAliasConfig } from './vis_type_alias';
|
||||
import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_action';
|
||||
import { visualizeFieldAction } from './trigger_actions/visualize_field_actions';
|
||||
import { visualizeTSVBAction } from './trigger_actions/visualize_tsvb_actions';
|
||||
|
||||
|
@ -259,6 +264,7 @@ export class LensPlugin {
|
|||
eventAnnotation
|
||||
);
|
||||
const visualizationMap = await this.editorFrameService!.loadVisualizations();
|
||||
const datasourceMap = await this.editorFrameService!.loadDatasources();
|
||||
|
||||
return {
|
||||
attributeService: getLensAttributeService(coreStart, plugins),
|
||||
|
@ -269,6 +275,7 @@ export class LensPlugin {
|
|||
documentToExpression: this.editorFrameService!.documentToExpression,
|
||||
injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
indexPatternService: plugins.dataViews,
|
||||
uiActions: plugins.uiActions,
|
||||
usageCollection,
|
||||
|
@ -440,6 +447,14 @@ export class LensPlugin {
|
|||
visualizeTSVBAction(core.application)
|
||||
);
|
||||
|
||||
startDependencies.uiActions.addTriggerAction(
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
createOpenInDiscoverAction(
|
||||
startDependencies.discover!,
|
||||
core.application.capabilities.discover.show as boolean
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
EmbeddableComponent: getEmbeddableComponent(core, startDependencies),
|
||||
SaveModalComponent: getSaveModalComponent(core, startDependencies),
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DiscoverStart } from '../../../../../src/plugins/discover/public';
|
||||
import type { IEmbeddable } from '../../../../../src/plugins/embeddable/public';
|
||||
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
|
||||
import { DOC_TYPE } from '../../common';
|
||||
import { Embeddable } from '../embeddable';
|
||||
import { createOpenInDiscoverAction } from './open_in_discover_action';
|
||||
|
||||
describe('open in discover action', () => {
|
||||
describe('compatibility check', () => {
|
||||
it('is incompatible with non-lens embeddables', async () => {
|
||||
const embeddable = { type: 'NOT_LENS' } as IEmbeddable;
|
||||
|
||||
const isCompatible = await createOpenInDiscoverAction({} as DiscoverStart, true).isCompatible(
|
||||
{
|
||||
embeddable,
|
||||
} as ActionExecutionContext<{ embeddable: IEmbeddable }>
|
||||
);
|
||||
|
||||
expect(isCompatible).toBeFalsy();
|
||||
});
|
||||
it('is incompatible if user cant access Discover app', async () => {
|
||||
// setup
|
||||
const embeddable = { type: DOC_TYPE } as Embeddable;
|
||||
embeddable.canViewUnderlyingData = () => Promise.resolve(true);
|
||||
|
||||
let hasDiscoverAccess = true;
|
||||
// make sure it would work if we had access to Discover
|
||||
expect(
|
||||
await createOpenInDiscoverAction({} as DiscoverStart, hasDiscoverAccess).isCompatible({
|
||||
embeddable,
|
||||
} as unknown as ActionExecutionContext<{ embeddable: IEmbeddable }>)
|
||||
).toBeTruthy();
|
||||
|
||||
// make sure no Discover access makes the action incompatible
|
||||
hasDiscoverAccess = false;
|
||||
expect(
|
||||
await createOpenInDiscoverAction({} as DiscoverStart, hasDiscoverAccess).isCompatible({
|
||||
embeddable,
|
||||
} as unknown as ActionExecutionContext<{ embeddable: IEmbeddable }>)
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('checks for ability to view underlying data if lens embeddable', async () => {
|
||||
// setup
|
||||
const embeddable = { type: DOC_TYPE } as Embeddable;
|
||||
|
||||
// test false
|
||||
embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(false));
|
||||
expect(
|
||||
await createOpenInDiscoverAction({} as DiscoverStart, true).isCompatible({
|
||||
embeddable,
|
||||
} as unknown as ActionExecutionContext<{ embeddable: IEmbeddable }>)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1);
|
||||
|
||||
// test true
|
||||
embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(true));
|
||||
expect(
|
||||
await createOpenInDiscoverAction({} as DiscoverStart, true).isCompatible({
|
||||
embeddable,
|
||||
} as unknown as ActionExecutionContext<{ embeddable: IEmbeddable }>)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to discover when executed', async () => {
|
||||
const viewUnderlyingDataArgs = {
|
||||
indexPatternId: 'index-pattern-id',
|
||||
timeRange: {},
|
||||
filters: [],
|
||||
query: undefined,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
const embeddable = {
|
||||
getViewUnderlyingDataArgs: jest.fn(() => viewUnderlyingDataArgs),
|
||||
};
|
||||
|
||||
const discoverUrl = 'https://discover-redirect-url';
|
||||
const discover = {
|
||||
locator: {
|
||||
getRedirectUrl: jest.fn(() => discoverUrl),
|
||||
},
|
||||
} as unknown as DiscoverStart;
|
||||
|
||||
globalThis.open = jest.fn();
|
||||
|
||||
await createOpenInDiscoverAction(discover, true).execute({
|
||||
embeddable,
|
||||
} as unknown as ActionExecutionContext<{
|
||||
embeddable: IEmbeddable;
|
||||
}>);
|
||||
|
||||
expect(embeddable.getViewUnderlyingDataArgs).toHaveBeenCalled();
|
||||
expect(discover.locator!.getRedirectUrl).toHaveBeenCalledWith(viewUnderlyingDataArgs);
|
||||
expect(globalThis.open).toHaveBeenCalledWith(discoverUrl, '_blank');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { IEmbeddable } from 'src/plugins/embeddable/public';
|
||||
import { createAction } from '../../../../../src/plugins/ui_actions/public';
|
||||
import type { Embeddable } from '../embeddable';
|
||||
import type { DiscoverStart } from '../../../../../src/plugins/discover/public';
|
||||
import { DOC_TYPE } from '../../common';
|
||||
|
||||
const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER';
|
||||
|
||||
export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverAccess: boolean) =>
|
||||
createAction<{ embeddable: IEmbeddable }>({
|
||||
type: ACTION_OPEN_IN_DISCOVER,
|
||||
id: ACTION_OPEN_IN_DISCOVER,
|
||||
order: 19, // right after Inspect which is 20
|
||||
getIconType: () => 'popout',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.lens.actions.openInDiscover', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
}),
|
||||
isCompatible: async (context: { embeddable: IEmbeddable }) => {
|
||||
if (!hasDiscoverAccess) return false;
|
||||
return (
|
||||
context.embeddable.type === DOC_TYPE &&
|
||||
(await (context.embeddable as Embeddable).canViewUnderlyingData())
|
||||
);
|
||||
},
|
||||
execute: async (context: { embeddable: Embeddable }) => {
|
||||
const args = context.embeddable.getViewUnderlyingDataArgs()!;
|
||||
const discoverUrl = discover.locator?.getRedirectUrl({
|
||||
...args,
|
||||
});
|
||||
window.open(discoverUrl, '_blank');
|
||||
},
|
||||
});
|
|
@ -91,8 +91,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
// Requires xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled
|
||||
// setting set in kibana.yml to work (not enabled by default)
|
||||
it('should be able to drill down to discover', async () => {
|
||||
// setting set in kibana.yml to test (not enabled by default)
|
||||
it('should hide old "explore underlying data" action', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await dashboardAddPanel.clickOpenAddPanel();
|
||||
|
@ -102,13 +102,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.goToTimeRange();
|
||||
await PageObjects.dashboard.saveDashboard('lnsDrilldown');
|
||||
await panelActions.openContextMenu();
|
||||
await testSubjects.clickWhenNotDisabled('embeddablePanelAction-ACTION_EXPLORE_DATA');
|
||||
await PageObjects.discover.waitForDiscoverAppOnScreen();
|
||||
|
||||
const el = await testSubjects.find('indexPattern-switch-link');
|
||||
const text = await el.getVisibleText();
|
||||
|
||||
expect(text).to.be('logstash-*');
|
||||
expect(await testSubjects.exists('embeddablePanelAction-ACTION_EXPLORE_DATA')).not.to.be.ok();
|
||||
});
|
||||
|
||||
it('should be able to add filters by clicking in pie chart', async () => {
|
||||
|
|
|
@ -58,6 +58,7 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid
|
|||
loadTestFile(require.resolve('./multi_terms'));
|
||||
loadTestFile(require.resolve('./epoch_millis'));
|
||||
loadTestFile(require.resolve('./show_underlying_data'));
|
||||
loadTestFile(require.resolve('./show_underlying_data_dashboard'));
|
||||
});
|
||||
|
||||
describe('', function () {
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import uuid from 'uuid';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects([
|
||||
'visualize',
|
||||
'lens',
|
||||
'dashboard',
|
||||
'header',
|
||||
'discover',
|
||||
'common',
|
||||
]);
|
||||
|
||||
const listingTable = getService('listingTable');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const filterBarService = getService('filterBar');
|
||||
const queryBar = getService('queryBar');
|
||||
const browser = getService('browser');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('lens show underlying data from dashboard', () => {
|
||||
it('should show the open button for a compatible saved visualization', async () => {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await listingTable.searchForItemWithName('lnsXYvis');
|
||||
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await PageObjects.lens.save('Embedded Visualization', true, false, false, 'new');
|
||||
|
||||
await PageObjects.dashboard.saveDashboard(`Open in Discover Testing ${uuid()}`, {
|
||||
exitFromEditMode: true,
|
||||
});
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
|
||||
await testSubjects.click('embeddablePanelAction-ACTION_OPEN_IN_DISCOVER');
|
||||
|
||||
const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles();
|
||||
await browser.switchToWindow(discoverWindowHandle);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.existOrFail('discoverChart');
|
||||
// check the table columns
|
||||
const columns = await PageObjects.discover.getColumnHeaders();
|
||||
expect(columns).to.eql(['ip', '@timestamp', 'bytes']);
|
||||
|
||||
await browser.closeCurrentWindow();
|
||||
await browser.switchToWindow(dashboardWindowHandle);
|
||||
});
|
||||
|
||||
it('should bring both dashboard context and visualization context to discover', async () => {
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
|
||||
await queryBar.switchQueryLanguage('lucene');
|
||||
await queryBar.setQuery('host.keyword www.elastic.co');
|
||||
await queryBar.submitQuery();
|
||||
await filterBarService.addFilter('geo.src', 'is', 'AF');
|
||||
// the filter bar seems to need a moment to settle before saving and returning
|
||||
await PageObjects.common.sleep(1000);
|
||||
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
|
||||
await queryBar.switchQueryLanguage('kql');
|
||||
await queryBar.setQuery('request.keyword : "/apm"');
|
||||
await queryBar.submitQuery();
|
||||
await filterBarService.addFilter(
|
||||
'host.raw',
|
||||
'is',
|
||||
'cdn.theacademyofperformingartsandscience.org'
|
||||
);
|
||||
|
||||
await PageObjects.dashboard.clickQuickSave();
|
||||
|
||||
// make sure Open in Discover is also available in edit mode
|
||||
await dashboardPanelActions.openContextMenuMorePanel();
|
||||
await testSubjects.existOrFail('embeddablePanelAction-ACTION_OPEN_IN_DISCOVER');
|
||||
|
||||
await PageObjects.dashboard.clickCancelOutOfEditMode();
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
|
||||
await testSubjects.click('embeddablePanelAction-ACTION_OPEN_IN_DISCOVER');
|
||||
|
||||
const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles();
|
||||
await browser.switchToWindow(discoverWindowHandle);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await retry.waitFor('filter count to be correct', async () => {
|
||||
const filterCount = await filterBarService.getFilterCount();
|
||||
return filterCount === 3;
|
||||
});
|
||||
|
||||
expect(
|
||||
await filterBarService.hasFilter('host.raw', 'cdn.theacademyofperformingartsandscience.org')
|
||||
).to.be.ok();
|
||||
expect(await filterBarService.hasFilter('geo.src', 'AF')).to.be.ok();
|
||||
expect(await filterBarService.getFiltersLabel()).to.contain('Lens context (lucene)');
|
||||
expect(await queryBar.getQueryString()).to.be('request.keyword : "/apm"');
|
||||
|
||||
await browser.closeCurrentWindow();
|
||||
await browser.switchToWindow(dashboardWindowHandle);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue