mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Embeddable] [Discover] Decouple Discover actions from Embeddable framework (#176953)
Part of https://github.com/elastic/kibana/issues/175138 ## Summary This PR decouples all Discover-owned actions (`ViewSavedSearchAction`, `ExploreDataChartAction`, and `ExploreDataContextMenuAction`) from the Embeddable framework. > [!NOTE] > In order to test the latter two actions, you must add the following to your `kibana.dev.yml`: >```yml > xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled: true > xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true > ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
edb11ebb36
commit
603b5f1069
15 changed files with 330 additions and 303 deletions
|
@ -6,21 +6,28 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { savedSearchMock } from '../__mocks__/saved_search';
|
||||
import { getDiscoverLocatorParams } from './get_discover_locator_params';
|
||||
import type { SearchInput } from './types';
|
||||
|
||||
describe('getDiscoverLocatorParams', () => {
|
||||
it('should return saved search id if input has savedObjectId', () => {
|
||||
const input = { savedObjectId: 'savedObjectId' } as SearchInput;
|
||||
expect(getDiscoverLocatorParams({ input, savedSearch: savedSearchMock })).toEqual({
|
||||
expect(
|
||||
getDiscoverLocatorParams({
|
||||
savedObjectId: new BehaviorSubject<string | undefined>('savedObjectId'),
|
||||
getSavedSearch: () => savedSearchMock,
|
||||
})
|
||||
).toEqual({
|
||||
savedSearchId: 'savedObjectId',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Discover params if input has no savedObjectId', () => {
|
||||
const input = {} as SearchInput;
|
||||
expect(getDiscoverLocatorParams({ input, savedSearch: savedSearchMock })).toEqual({
|
||||
expect(
|
||||
getDiscoverLocatorParams({
|
||||
getSavedSearch: () => savedSearchMock,
|
||||
})
|
||||
).toEqual({
|
||||
dataViewId: savedSearchMock.searchSource.getField('index')?.id,
|
||||
dataViewSpec: savedSearchMock.searchSource.getField('index')?.toMinimalSpec(),
|
||||
timeRange: savedSearchMock.timeRange,
|
||||
|
|
|
@ -7,34 +7,31 @@
|
|||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/common';
|
||||
import type { SearchByReferenceInput } from '@kbn/saved-search-plugin/public';
|
||||
import { PublishesLocalUnifiedSearch, PublishesSavedObjectId } from '@kbn/presentation-publishing';
|
||||
import type { DiscoverAppLocatorParams } from '../../common';
|
||||
import type { SearchInput } from './types';
|
||||
import { HasSavedSearch } from './types';
|
||||
|
||||
export const getDiscoverLocatorParams = ({
|
||||
input,
|
||||
savedSearch,
|
||||
}: {
|
||||
input: SearchInput;
|
||||
savedSearch: SavedSearch;
|
||||
}) => {
|
||||
const dataView = savedSearch.searchSource.getField('index');
|
||||
const savedObjectId = (input as SearchByReferenceInput).savedObjectId;
|
||||
export const getDiscoverLocatorParams = (
|
||||
api: HasSavedSearch & Partial<PublishesSavedObjectId & PublishesLocalUnifiedSearch>
|
||||
) => {
|
||||
const savedSearch = api.getSavedSearch();
|
||||
|
||||
const dataView = savedSearch?.searchSource.getField('index');
|
||||
const savedObjectId = api.savedObjectId?.getValue();
|
||||
const locatorParams: DiscoverAppLocatorParams = savedObjectId
|
||||
? { savedSearchId: savedObjectId }
|
||||
: {
|
||||
dataViewId: dataView?.id,
|
||||
dataViewSpec: dataView?.toMinimalSpec(),
|
||||
timeRange: savedSearch.timeRange,
|
||||
refreshInterval: savedSearch.refreshInterval,
|
||||
filters: savedSearch.searchSource.getField('filter') as Filter[],
|
||||
query: savedSearch.searchSource.getField('query'),
|
||||
columns: savedSearch.columns,
|
||||
sort: savedSearch.sort,
|
||||
viewMode: savedSearch.viewMode,
|
||||
hideAggregatedPreview: savedSearch.hideAggregatedPreview,
|
||||
breakdownField: savedSearch.breakdownField,
|
||||
timeRange: savedSearch?.timeRange,
|
||||
refreshInterval: savedSearch?.refreshInterval,
|
||||
filters: savedSearch?.searchSource.getField('filter') as Filter[],
|
||||
query: savedSearch?.searchSource.getField('query'),
|
||||
columns: savedSearch?.columns,
|
||||
sort: savedSearch?.sort,
|
||||
viewMode: savedSearch?.viewMode,
|
||||
hideAggregatedPreview: savedSearch?.hideAggregatedPreview,
|
||||
breakdownField: savedSearch?.breakdownField,
|
||||
};
|
||||
|
||||
return locatorParams;
|
||||
|
|
|
@ -6,26 +6,26 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { SearchInput } from '..';
|
||||
import { DiscoverServices } from '../build_services';
|
||||
import { discoverServiceMock } from '../__mocks__/services';
|
||||
import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable';
|
||||
import { render } from 'react-dom';
|
||||
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
|
||||
import { VIEW_MODE } from '../../common/constants';
|
||||
import { buildDataViewMock, deepMockedFields } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { getDiscoverLocatorParams } from './get_discover_locator_params';
|
||||
import { dataViewAdHoc } from '../__mocks__/data_view_complex';
|
||||
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { SavedSearchByValueAttributes } from '@kbn/saved-search-plugin/public';
|
||||
import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
|
||||
import { buildDataViewMock, deepMockedFields } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { SavedSearchByValueAttributes } from '@kbn/saved-search-plugin/public';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { ReactElement } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { SearchInput } from '..';
|
||||
import { VIEW_MODE } from '../../common/constants';
|
||||
import { DiscoverServices } from '../build_services';
|
||||
import { dataViewAdHoc } from '../__mocks__/data_view_complex';
|
||||
import { discoverServiceMock } from '../__mocks__/services';
|
||||
import { getDiscoverLocatorParams } from './get_discover_locator_params';
|
||||
import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable';
|
||||
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
|
||||
|
||||
jest.mock('./get_discover_locator_params', () => {
|
||||
const actual = jest.requireActual('./get_discover_locator_params');
|
||||
|
@ -418,16 +418,13 @@ describe('saved search embeddable', () => {
|
|||
.spyOn(servicesMock.core.http.basePath, 'remove')
|
||||
.mockClear()
|
||||
.mockReturnValueOnce('/mock-url');
|
||||
const { embeddable, searchInput, savedSearch } = createEmbeddable({ dataView, byValue });
|
||||
const getLocatorParamsArgs = {
|
||||
input: searchInput,
|
||||
savedSearch,
|
||||
};
|
||||
const locatorParams = getDiscoverLocatorParams(getLocatorParamsArgs);
|
||||
const { embeddable } = createEmbeddable({ dataView, byValue });
|
||||
|
||||
const locatorParams = getDiscoverLocatorParams(embeddable);
|
||||
(getDiscoverLocatorParams as jest.Mock).mockClear();
|
||||
await waitOneTick();
|
||||
expect(getDiscoverLocatorParams).toHaveBeenCalledTimes(1);
|
||||
expect(getDiscoverLocatorParams).toHaveBeenCalledWith(getLocatorParamsArgs);
|
||||
expect(getDiscoverLocatorParams).toHaveBeenCalledWith(embeddable);
|
||||
expect(servicesMock.locator.getUrl).toHaveBeenCalledTimes(1);
|
||||
expect(servicesMock.locator.getUrl).toHaveBeenCalledWith(locatorParams);
|
||||
expect(servicesMock.core.http.basePath.remove).toHaveBeenCalledTimes(1);
|
||||
|
@ -459,19 +456,15 @@ describe('saved search embeddable', () => {
|
|||
.spyOn(servicesMock.core.http.basePath, 'remove')
|
||||
.mockClear()
|
||||
.mockReturnValueOnce('/mock-url');
|
||||
const { embeddable, searchInput, savedSearch } = createEmbeddable({
|
||||
const { embeddable } = createEmbeddable({
|
||||
dataView: dataViewAdHoc,
|
||||
byValue: true,
|
||||
});
|
||||
const getLocatorParamsArgs = {
|
||||
input: searchInput,
|
||||
savedSearch,
|
||||
};
|
||||
const locatorParams = getDiscoverLocatorParams(getLocatorParamsArgs);
|
||||
const locatorParams = getDiscoverLocatorParams(embeddable);
|
||||
(getDiscoverLocatorParams as jest.Mock).mockClear();
|
||||
await waitOneTick();
|
||||
expect(getDiscoverLocatorParams).toHaveBeenCalledTimes(1);
|
||||
expect(getDiscoverLocatorParams).toHaveBeenCalledWith(getLocatorParamsArgs);
|
||||
expect(getDiscoverLocatorParams).toHaveBeenCalledWith(embeddable);
|
||||
expect(servicesMock.locator.getRedirectUrl).toHaveBeenCalledTimes(1);
|
||||
expect(servicesMock.locator.getRedirectUrl).toHaveBeenCalledWith(locatorParams);
|
||||
expect(servicesMock.core.http.basePath.remove).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -185,7 +185,7 @@ export class SavedSearchEmbeddable
|
|||
const title = this.getCurrentTitle();
|
||||
const description = input.hidePanelTitles ? '' : input.description ?? savedSearch.description;
|
||||
const savedObjectId = (input as SearchByReferenceInput).savedObjectId;
|
||||
const locatorParams = getDiscoverLocatorParams({ input, savedSearch });
|
||||
const locatorParams = getDiscoverLocatorParams(this);
|
||||
// We need to use a redirect URL if this is a by value saved search using
|
||||
// an ad hoc data view to ensure the data view spec gets encoded in the URL
|
||||
const useRedirect = !savedObjectId && !dataView?.isPersisted();
|
||||
|
|
|
@ -6,17 +6,20 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Embeddable, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { Embeddable, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type {
|
||||
SavedSearch,
|
||||
SearchByReferenceInput,
|
||||
SearchByValueInput,
|
||||
} from '@kbn/saved-search-plugin/public';
|
||||
|
||||
import type { Adapters } from '@kbn/embeddable-plugin/public';
|
||||
import type { DiscoverGridEmbeddableSearchProps } from './saved_search_grid';
|
||||
import type { DocTableEmbeddableSearchProps } from '../components/doc_table/doc_table_embeddable';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
|
||||
import type { DiscoverServices } from '../build_services';
|
||||
import type { DocTableEmbeddableSearchProps } from '../components/doc_table/doc_table_embeddable';
|
||||
import type { DiscoverGridEmbeddableSearchProps } from './saved_search_grid';
|
||||
|
||||
export type SearchInput = SearchByValueInput | SearchByReferenceInput;
|
||||
|
||||
|
@ -25,17 +28,32 @@ export interface SearchOutput extends EmbeddableOutput {
|
|||
editable: boolean;
|
||||
}
|
||||
|
||||
export interface ISearchEmbeddable extends IEmbeddable<SearchInput, SearchOutput> {
|
||||
getSavedSearch(): SavedSearch | undefined;
|
||||
hasTimeRange(): boolean;
|
||||
}
|
||||
export type ISearchEmbeddable = IEmbeddable<SearchInput, SearchOutput> &
|
||||
HasSavedSearch &
|
||||
HasTimeRange;
|
||||
|
||||
export interface SearchEmbeddable extends Embeddable<SearchInput, SearchOutput> {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface HasSavedSearch {
|
||||
getSavedSearch: () => SavedSearch | undefined;
|
||||
}
|
||||
|
||||
export const apiHasSavedSearch = (
|
||||
api: EmbeddableApiContext['embeddable']
|
||||
): api is HasSavedSearch => {
|
||||
const embeddable = api as HasSavedSearch;
|
||||
return Boolean(embeddable.getSavedSearch) && typeof embeddable.getSavedSearch === 'function';
|
||||
};
|
||||
|
||||
export interface HasTimeRange {
|
||||
hasTimeRange(): boolean;
|
||||
}
|
||||
|
||||
export type EmbeddableComponentSearchProps = DiscoverGridEmbeddableSearchProps &
|
||||
DocTableEmbeddableSearchProps;
|
||||
|
||||
export type SearchProps = EmbeddableComponentSearchProps & {
|
||||
sampleSizeState: number | undefined;
|
||||
description?: string;
|
||||
|
|
|
@ -28,7 +28,6 @@ const searchInput = {
|
|||
const executeTriggerActions = async (triggerId: string, context: object) => {
|
||||
return Promise.resolve(undefined);
|
||||
};
|
||||
const trigger = { id: 'ACTION_VIEW_SAVED_SEARCH' };
|
||||
const embeddableConfig = {
|
||||
editable: true,
|
||||
services,
|
||||
|
@ -39,7 +38,7 @@ describe('view saved search action', () => {
|
|||
it('is compatible when embeddable is of type saved search, in view mode && appropriate permissions are set', async () => {
|
||||
const action = new ViewSavedSearchAction(applicationMock, services.locator);
|
||||
const embeddable = new SavedSearchEmbeddable(embeddableConfig, searchInput);
|
||||
expect(await action.isCompatible({ embeddable, trigger })).toBe(true);
|
||||
expect(await action.isCompatible({ embeddable })).toBe(true);
|
||||
});
|
||||
|
||||
it('is not compatible when embeddable not of type saved search', async () => {
|
||||
|
@ -57,7 +56,6 @@ describe('view saved search action', () => {
|
|||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable,
|
||||
trigger,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
@ -69,7 +67,6 @@ describe('view saved search action', () => {
|
|||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable,
|
||||
trigger,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
@ -78,12 +75,9 @@ describe('view saved search action', () => {
|
|||
const action = new ViewSavedSearchAction(applicationMock, services.locator);
|
||||
const embeddable = new SavedSearchEmbeddable(embeddableConfig, searchInput);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await action.execute({ embeddable, trigger });
|
||||
await action.execute({ embeddable });
|
||||
expect(discoverServiceMock.locator.navigate).toHaveBeenCalledWith(
|
||||
getDiscoverLocatorParams({
|
||||
input: embeddable.getInput(),
|
||||
savedSearch: embeddable.getSavedSearch()!,
|
||||
})
|
||||
getDiscoverLocatorParams(embeddable)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,23 +6,42 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type IEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils';
|
||||
import type { SavedSearchEmbeddable } from './saved_search_embeddable';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
apiCanAccessViewMode,
|
||||
apiHasType,
|
||||
apiIsOfType,
|
||||
CanAccessViewMode,
|
||||
EmbeddableApiContext,
|
||||
getInheritedViewMode,
|
||||
HasType,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import type { Action } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import type { DiscoverAppLocator } from '../../common';
|
||||
import { getDiscoverLocatorParams } from './get_discover_locator_params';
|
||||
import { apiHasSavedSearch, HasSavedSearch } from './types';
|
||||
|
||||
export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH';
|
||||
|
||||
export interface ViewSearchContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
type ViewSavedSearchActionApi = CanAccessViewMode & HasType & HasSavedSearch;
|
||||
|
||||
export class ViewSavedSearchAction implements Action<ViewSearchContext> {
|
||||
const compatibilityCheck = (
|
||||
api: EmbeddableApiContext['embeddable']
|
||||
): api is ViewSavedSearchActionApi => {
|
||||
return (
|
||||
apiCanAccessViewMode(api) &&
|
||||
getInheritedViewMode(api) === ViewMode.VIEW &&
|
||||
apiHasType(api) &&
|
||||
apiIsOfType(api, SEARCH_EMBEDDABLE_TYPE) &&
|
||||
apiHasSavedSearch(api)
|
||||
);
|
||||
};
|
||||
|
||||
export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
|
||||
public id = ACTION_VIEW_SAVED_SEARCH;
|
||||
public readonly type = ACTION_VIEW_SAVED_SEARCH;
|
||||
|
||||
|
@ -31,38 +50,29 @@ export class ViewSavedSearchAction implements Action<ViewSearchContext> {
|
|||
private readonly locator: DiscoverAppLocator
|
||||
) {}
|
||||
|
||||
async execute(context: ActionExecutionContext<ViewSearchContext>): Promise<void> {
|
||||
const embeddable = context.embeddable as SavedSearchEmbeddable;
|
||||
const savedSearch = embeddable.getSavedSearch();
|
||||
if (!savedSearch) {
|
||||
async execute({ embeddable }: EmbeddableApiContext): Promise<void> {
|
||||
if (!compatibilityCheck(embeddable)) {
|
||||
return;
|
||||
}
|
||||
const locatorParams = getDiscoverLocatorParams({
|
||||
input: embeddable.getInput(),
|
||||
savedSearch,
|
||||
});
|
||||
|
||||
const locatorParams = getDiscoverLocatorParams(embeddable);
|
||||
await this.locator.navigate(locatorParams);
|
||||
}
|
||||
|
||||
getDisplayName(context: ActionExecutionContext<ViewSearchContext>): string {
|
||||
getDisplayName(): string {
|
||||
return i18n.translate('discover.savedSearchEmbeddable.action.viewSavedSearch.displayName', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
});
|
||||
}
|
||||
|
||||
getIconType(context: ActionExecutionContext<ViewSearchContext>): string | undefined {
|
||||
getIconType(): string | undefined {
|
||||
return 'inspect';
|
||||
}
|
||||
|
||||
async isCompatible(context: ActionExecutionContext<ViewSearchContext>) {
|
||||
const { embeddable } = context;
|
||||
async isCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
const { capabilities } = this.application;
|
||||
const hasDiscoverPermissions =
|
||||
(capabilities.discover.show as boolean) || (capabilities.discover.save as boolean);
|
||||
return Boolean(
|
||||
embeddable.type === SEARCH_EMBEDDABLE_TYPE &&
|
||||
embeddable.getInput().viewMode === ViewMode.VIEW &&
|
||||
hasDiscoverPermissions
|
||||
);
|
||||
return compatibilityCheck(embeddable) && hasDiscoverPermissions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
"@kbn/managed-content-badge",
|
||||
"@kbn/deeplinks-analytics",
|
||||
"@kbn/shared-ux-markdown",
|
||||
"@kbn/data-view-utils"
|
||||
"@kbn/data-view-utils",
|
||||
"@kbn/presentation-publishing"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -5,13 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import { ViewMode, IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaLocation } from '@kbn/share-plugin/public';
|
||||
import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DOC_TYPE as LENS_DOC_TYPE } from '@kbn/lens-plugin/common/constants';
|
||||
import {
|
||||
apiCanAccessViewMode,
|
||||
apiHasParentApi,
|
||||
apiHasType,
|
||||
apiIsOfType,
|
||||
apiPublishesDataViews,
|
||||
apiPublishesPartialLocalUnifiedSearch,
|
||||
CanAccessViewMode,
|
||||
EmbeddableApiContext,
|
||||
getInheritedViewMode,
|
||||
HasType,
|
||||
PublishesDataViews,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { KibanaLocation } from '@kbn/share-plugin/public';
|
||||
|
||||
import * as shared from './shared';
|
||||
|
||||
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
||||
|
@ -28,54 +43,108 @@ export interface Params {
|
|||
start: StartServicesGetter<PluginDeps, unknown, CoreDeps>;
|
||||
}
|
||||
|
||||
export abstract class AbstractExploreDataAction<Context extends { embeddable?: IEmbeddable }> {
|
||||
public readonly getIconType = (context: Context): string => 'discoverApp';
|
||||
type AbstractExploreDataActionApi = CanAccessViewMode & HasType & PublishesDataViews;
|
||||
|
||||
public readonly getDisplayName = (context: Context): string =>
|
||||
const isApiCompatible = (api: unknown | null): api is AbstractExploreDataActionApi =>
|
||||
apiCanAccessViewMode(api) && apiHasType(api) && apiPublishesDataViews(api);
|
||||
|
||||
const compatibilityCheck = (api: EmbeddableApiContext['embeddable']) => {
|
||||
return (
|
||||
isApiCompatible(api) &&
|
||||
getInheritedViewMode(api) === ViewMode.VIEW &&
|
||||
!apiIsOfType(api, LENS_DOC_TYPE)
|
||||
);
|
||||
};
|
||||
|
||||
export abstract class AbstractExploreDataAction {
|
||||
public readonly getIconType = (): string => 'discoverApp';
|
||||
|
||||
public readonly getDisplayName = (): string =>
|
||||
i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', {
|
||||
defaultMessage: 'Explore underlying data',
|
||||
});
|
||||
|
||||
constructor(protected readonly params: Params) {}
|
||||
|
||||
protected abstract getLocation(context: Context): Promise<KibanaLocation>;
|
||||
protected async getLocation(
|
||||
{ embeddable }: EmbeddableApiContext,
|
||||
eventParams?: DiscoverAppLocatorParams
|
||||
): Promise<KibanaLocation> {
|
||||
const { plugins } = this.params.start();
|
||||
const { locator } = plugins.discover;
|
||||
|
||||
public async isCompatible({ embeddable }: Context): Promise<boolean> {
|
||||
if (!embeddable) return false;
|
||||
if (embeddable.type === LENS_DOC_TYPE) return false;
|
||||
if (!locator) {
|
||||
throw new Error('Discover URL locator not available.');
|
||||
}
|
||||
|
||||
const parentParams: DiscoverAppLocatorParams = {};
|
||||
if (
|
||||
apiHasParentApi(embeddable) &&
|
||||
apiPublishesPartialLocalUnifiedSearch(embeddable.parentApi)
|
||||
) {
|
||||
parentParams.filters = embeddable.parentApi.localFilters?.getValue() ?? [];
|
||||
parentParams.query = embeddable.parentApi.localQuery?.getValue();
|
||||
parentParams.timeRange = embeddable.parentApi.localTimeRange?.getValue();
|
||||
}
|
||||
|
||||
const childParams: DiscoverAppLocatorParams = {};
|
||||
if (apiPublishesPartialLocalUnifiedSearch(embeddable)) {
|
||||
childParams.filters = embeddable.localFilters?.getValue() ?? [];
|
||||
childParams.query = embeddable.localQuery?.getValue();
|
||||
childParams.timeRange = embeddable.localTimeRange?.getValue();
|
||||
}
|
||||
|
||||
const params: DiscoverAppLocatorParams = {
|
||||
dataViewId: shared.getDataViews(embeddable)[0],
|
||||
filters: [
|
||||
// combine filters from all possible sources
|
||||
...(parentParams.filters ?? []),
|
||||
...(childParams.filters ?? []),
|
||||
...(eventParams?.filters ?? []),
|
||||
],
|
||||
query: parentParams.query ?? childParams.query, // overwrite the child query with the parent query
|
||||
// prioritize event time range for chart action; otherwise, overwrite the parent time range with the child's
|
||||
timeRange: eventParams?.timeRange ?? childParams.timeRange ?? parentParams.timeRange,
|
||||
};
|
||||
|
||||
const location = await locator.getLocation(params);
|
||||
return location;
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableApiContext): Promise<boolean> {
|
||||
if (!compatibilityCheck(embeddable)) return false;
|
||||
|
||||
const { core, plugins } = this.params.start();
|
||||
const { capabilities } = core.application;
|
||||
|
||||
if (capabilities.discover && !capabilities.discover.show) return false;
|
||||
if (!plugins.discover.locator) return false;
|
||||
if (!shared.hasExactlyOneIndexPattern(embeddable)) return false;
|
||||
if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false;
|
||||
|
||||
return true;
|
||||
return shared.hasExactlyOneDataView(embeddable);
|
||||
}
|
||||
|
||||
public async execute(context: Context): Promise<void> {
|
||||
if (!shared.hasExactlyOneIndexPattern(context.embeddable)) return;
|
||||
public async execute(api: EmbeddableApiContext): Promise<void> {
|
||||
const { embeddable } = api;
|
||||
if (!this.isCompatible({ embeddable })) return;
|
||||
|
||||
const { core } = this.params.start();
|
||||
const { app, path } = await this.getLocation(context);
|
||||
const { app, path } = await this.getLocation(api);
|
||||
|
||||
await core.application.navigateToApp(app, {
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
public async getHref(context: Context): Promise<string> {
|
||||
const { embeddable } = context;
|
||||
public async getHref(api: EmbeddableApiContext): Promise<string> {
|
||||
const { embeddable } = api;
|
||||
|
||||
if (!shared.hasExactlyOneIndexPattern(embeddable)) {
|
||||
throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`);
|
||||
if (!this.isCompatible({ embeddable })) {
|
||||
throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`);
|
||||
}
|
||||
|
||||
const { core } = this.params.start();
|
||||
const { app, path } = await this.getLocation(context);
|
||||
const url = await core.application.getUrlForApp(app, { path, absolute: false });
|
||||
const { app, path } = await this.getLocation(api);
|
||||
const url = core.application.getUrlForApp(app, { path, absolute: false });
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -4,19 +4,21 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { Filter, RangeFilter } from '@kbn/es-query';
|
||||
import { ExploreDataChartAction } from './explore_data_chart_action';
|
||||
import { Params, PluginDeps } from './abstract_explore_data_action';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { ExploreDataChartActionContext } from './explore_data_chart_action';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DiscoverAppLocator } from '@kbn/discover-plugin/common';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { Filter, RangeFilter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewMode as ViewModeType } from '@kbn/presentation-publishing';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import {
|
||||
VisualizeEmbeddableContract,
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
} from '@kbn/visualizations-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { DiscoverAppLocator } from '@kbn/discover-plugin/common';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Params, PluginDeps } from './abstract_explore_data_action';
|
||||
import { ExploreDataChartAction, ExploreDataChartActionContext } from './explore_data_chart_action';
|
||||
|
||||
const i18nTranslateSpy = i18n.translate as unknown as jest.SpyInstance;
|
||||
|
||||
|
@ -68,22 +70,17 @@ const setup = (
|
|||
};
|
||||
const action = new ExploreDataChartAction(params);
|
||||
|
||||
const input = {
|
||||
viewMode: ViewMode.VIEW,
|
||||
};
|
||||
|
||||
const output = {
|
||||
indexPatterns: [
|
||||
const embeddable: VisualizeEmbeddableContract = {
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
dataViews: new BehaviorSubject([
|
||||
{
|
||||
id: 'index-ptr-foo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const embeddable: VisualizeEmbeddableContract = {
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
getInput: () => input,
|
||||
getOutput: () => output,
|
||||
]),
|
||||
localFilters: new BehaviorSubject([]),
|
||||
parentApi: {
|
||||
viewMode: new BehaviorSubject(ViewMode.VIEW),
|
||||
},
|
||||
} as unknown as VisualizeEmbeddableContract;
|
||||
|
||||
const context = {
|
||||
|
@ -92,25 +89,25 @@ const setup = (
|
|||
embeddable,
|
||||
} as ExploreDataChartActionContext;
|
||||
|
||||
return { core, plugins, locator, params, action, input, output, embeddable, context };
|
||||
return { core, plugins, locator, params, action, embeddable, context };
|
||||
};
|
||||
|
||||
describe('"Explore underlying data" panel action', () => {
|
||||
test('action has Discover icon', () => {
|
||||
const { action, context } = setup();
|
||||
expect(action.getIconType(context)).toBe('discoverApp');
|
||||
const { action } = setup();
|
||||
expect(action.getIconType()).toBe('discoverApp');
|
||||
});
|
||||
|
||||
test('title is "Explore underlying data"', () => {
|
||||
const { action, context } = setup();
|
||||
expect(action.getDisplayName(context)).toBe('Explore underlying data');
|
||||
const { action } = setup();
|
||||
expect(action.getDisplayName()).toBe('Explore underlying data');
|
||||
});
|
||||
|
||||
test('translates title', () => {
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const { action, context } = setup();
|
||||
action.getDisplayName(context);
|
||||
const { action } = setup();
|
||||
action.getDisplayName();
|
||||
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(i18nTranslateSpy.mock.calls[0][0]).toBe(
|
||||
|
@ -136,35 +133,35 @@ describe('"Explore underlying data" panel action', () => {
|
|||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if embeddable has more than one index pattern', async () => {
|
||||
const { action, output, context } = setup();
|
||||
output.indexPatterns = [
|
||||
test('returns false if embeddable has more than one data view', async () => {
|
||||
const { action, embeddable, context } = setup();
|
||||
embeddable.dataViews = new BehaviorSubject<undefined | DataView[]>([
|
||||
{
|
||||
id: 'index-ptr-foo',
|
||||
},
|
||||
{
|
||||
id: 'index-ptr-bar',
|
||||
},
|
||||
];
|
||||
] as any as DataView[]);
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if embeddable does not have index patterns', async () => {
|
||||
const { action, output, context } = setup();
|
||||
test('returns false if embeddable does not have data views', async () => {
|
||||
const { action, embeddable, context } = setup();
|
||||
// @ts-expect-error
|
||||
delete output.indexPatterns;
|
||||
embeddable.dataViews = undefined;
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if embeddable index patterns are empty', async () => {
|
||||
const { action, output, context } = setup();
|
||||
output.indexPatterns = [];
|
||||
test('returns false if embeddable data views are empty', async () => {
|
||||
const { action, embeddable, context } = setup();
|
||||
embeddable.dataViews = new BehaviorSubject<undefined | DataView[]>([]);
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
|
@ -172,9 +169,10 @@ describe('"Explore underlying data" panel action', () => {
|
|||
});
|
||||
|
||||
test('returns false if dashboard is in edit mode', async () => {
|
||||
const { action, input, context } = setup();
|
||||
input.viewMode = ViewMode.EDIT;
|
||||
|
||||
const { action, embeddable, context } = setup();
|
||||
if (embeddable.parentApi) {
|
||||
embeddable.parentApi.viewMode = new BehaviorSubject<ViewModeType>(ViewMode.EDIT);
|
||||
}
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
|
@ -189,7 +187,6 @@ describe('"Explore underlying data" panel action', () => {
|
|||
};
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -205,7 +202,7 @@ describe('"Explore underlying data" panel action', () => {
|
|||
expect(locator.getLocation).toHaveBeenCalledTimes(1);
|
||||
expect(locator.getLocation).toHaveBeenCalledWith({
|
||||
filters: [],
|
||||
indexPatternId: 'index-ptr-foo',
|
||||
dataViewId: 'index-ptr-foo',
|
||||
timeRange: undefined,
|
||||
});
|
||||
});
|
||||
|
@ -258,7 +255,7 @@ describe('"Explore underlying data" panel action', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
indexPatternId: 'index-ptr-foo',
|
||||
dataViewId: 'index-ptr-foo',
|
||||
timeRange: {
|
||||
from,
|
||||
to,
|
||||
|
|
|
@ -5,27 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import { SearchInput } from '@kbn/discover-plugin/public';
|
||||
import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public';
|
||||
import { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
apiIsOfType,
|
||||
apiPublishesPartialLocalUnifiedSearch,
|
||||
HasParentApi,
|
||||
PublishesLocalUnifiedSearch,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { KibanaLocation } from '@kbn/share-plugin/public';
|
||||
import * as shared from './shared';
|
||||
import { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public';
|
||||
import { AbstractExploreDataAction } from './abstract_explore_data_action';
|
||||
|
||||
export interface ExploreDataChartActionContext extends ApplyGlobalFilterActionContext {
|
||||
embeddable?: IEmbeddable;
|
||||
}
|
||||
|
||||
export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART';
|
||||
|
||||
export interface ExploreDataChartActionContext extends ApplyGlobalFilterActionContext {
|
||||
embeddable: Partial<PublishesLocalUnifiedSearch> &
|
||||
Partial<HasParentApi<Partial<PublishesLocalUnifiedSearch>>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "Explore underlying data" action which appears in popup context
|
||||
* menu when user clicks a value in visualization or brushes a time range.
|
||||
*/
|
||||
|
||||
export class ExploreDataChartAction
|
||||
extends AbstractExploreDataAction<ExploreDataChartActionContext>
|
||||
extends AbstractExploreDataAction
|
||||
implements Action<ExploreDataChartActionContext>
|
||||
{
|
||||
public readonly id = ACTION_EXPLORE_DATA_CHART;
|
||||
|
@ -35,44 +39,22 @@ export class ExploreDataChartAction
|
|||
public readonly order = 200;
|
||||
|
||||
public async isCompatible(context: ExploreDataChartActionContext): Promise<boolean> {
|
||||
if (context.embeddable?.type === 'map') return false; // TODO: https://github.com/elastic/kibana/issues/73043
|
||||
return super.isCompatible(context);
|
||||
const { embeddable } = context;
|
||||
if (apiIsOfType(embeddable, 'map')) {
|
||||
return false; // TODO: https://github.com/elastic/kibana/issues/73043
|
||||
}
|
||||
return apiPublishesPartialLocalUnifiedSearch(embeddable) && super.isCompatible(context);
|
||||
}
|
||||
|
||||
protected readonly getLocation = async (
|
||||
context: ExploreDataChartActionContext
|
||||
): Promise<KibanaLocation> => {
|
||||
const { plugins } = this.params.start();
|
||||
const { locator } = plugins.discover;
|
||||
|
||||
if (!locator) {
|
||||
throw new Error('Discover URL locator not available.');
|
||||
}
|
||||
|
||||
const { embeddable } = context;
|
||||
const { extractTimeRange } = await import('@kbn/es-query');
|
||||
const { restOfFilters: filters, timeRange } = extractTimeRange(
|
||||
context.filters,
|
||||
context.filters ?? [],
|
||||
context.timeFieldName
|
||||
);
|
||||
|
||||
const params: DiscoverAppLocatorParams = {
|
||||
filters,
|
||||
timeRange,
|
||||
};
|
||||
|
||||
if (embeddable) {
|
||||
params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
|
||||
|
||||
const input = embeddable.getInput() as Readonly<SearchInput>;
|
||||
|
||||
if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange;
|
||||
if (input.query) params.query = input.query;
|
||||
if (input.filters) params.filters = [...input.filters, ...(params.filters || [])];
|
||||
}
|
||||
|
||||
const location = await locator.getLocation(params);
|
||||
|
||||
return location;
|
||||
return super.getLocation(context, { filters, timeRange });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,17 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExploreDataContextMenuAction } from './explore_data_context_menu_action';
|
||||
import { Params, PluginDeps } from './abstract_explore_data_action';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DiscoverAppLocator } from '@kbn/discover-plugin/common';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewMode as ViewModeType } from '@kbn/presentation-publishing';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import {
|
||||
VisualizeEmbeddableContract,
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
} from '@kbn/visualizations-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { DiscoverAppLocator } from '@kbn/discover-plugin/common';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Params, PluginDeps } from './abstract_explore_data_action';
|
||||
import { ExploreDataContextMenuAction } from './explore_data_context_menu_action';
|
||||
|
||||
const i18nTranslateSpy = i18n.translate as unknown as jest.SpyInstance;
|
||||
|
||||
|
@ -57,47 +60,42 @@ const setup = () => {
|
|||
};
|
||||
const action = new ExploreDataContextMenuAction(params);
|
||||
|
||||
const input = {
|
||||
viewMode: ViewMode.VIEW,
|
||||
};
|
||||
|
||||
const output = {
|
||||
indexPatterns: [
|
||||
const embeddable: VisualizeEmbeddableContract = {
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
dataViews: new BehaviorSubject([
|
||||
{
|
||||
id: 'index-ptr-foo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const embeddable: VisualizeEmbeddableContract = {
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
getInput: () => input,
|
||||
getOutput: () => output,
|
||||
]),
|
||||
parentApi: {
|
||||
viewMode: new BehaviorSubject(ViewMode.VIEW),
|
||||
localFilters: new BehaviorSubject([]),
|
||||
},
|
||||
} as unknown as VisualizeEmbeddableContract;
|
||||
|
||||
const context = {
|
||||
embeddable,
|
||||
};
|
||||
|
||||
return { core, plugins, locator, params, action, input, output, embeddable, context };
|
||||
return { core, plugins, locator, params, action, embeddable, context };
|
||||
};
|
||||
|
||||
describe('"Explore underlying data" panel action', () => {
|
||||
test('action has Discover icon', () => {
|
||||
const { action, context } = setup();
|
||||
expect(action.getIconType(context)).toBe('discoverApp');
|
||||
const { action } = setup();
|
||||
expect(action.getIconType()).toBe('discoverApp');
|
||||
});
|
||||
|
||||
test('title is "Explore underlying data"', () => {
|
||||
const { action, context } = setup();
|
||||
expect(action.getDisplayName(context)).toBe('Explore underlying data');
|
||||
const { action } = setup();
|
||||
expect(action.getDisplayName()).toBe('Explore underlying data');
|
||||
});
|
||||
|
||||
test('translates title', () => {
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const { action, context } = setup();
|
||||
action.getDisplayName(context);
|
||||
const { action } = setup();
|
||||
action.getDisplayName();
|
||||
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(i18nTranslateSpy.mock.calls[0][0]).toBe(
|
||||
|
@ -124,15 +122,15 @@ describe('"Explore underlying data" panel action', () => {
|
|||
});
|
||||
|
||||
test('returns false if embeddable has more than one index pattern', async () => {
|
||||
const { action, output, context } = setup();
|
||||
output.indexPatterns = [
|
||||
const { action, embeddable, context } = setup();
|
||||
embeddable.dataViews = new BehaviorSubject<undefined | DataView[]>([
|
||||
{
|
||||
id: 'index-ptr-foo',
|
||||
},
|
||||
{
|
||||
id: 'index-ptr-bar',
|
||||
},
|
||||
];
|
||||
] as any as DataView[]);
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
|
@ -140,9 +138,9 @@ describe('"Explore underlying data" panel action', () => {
|
|||
});
|
||||
|
||||
test('returns false if embeddable does not have index patterns', async () => {
|
||||
const { action, output, context } = setup();
|
||||
const { action, embeddable, context } = setup();
|
||||
// @ts-expect-error
|
||||
delete output.indexPatterns;
|
||||
embeddable.dataViews = undefined;
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
|
@ -150,8 +148,8 @@ describe('"Explore underlying data" panel action', () => {
|
|||
});
|
||||
|
||||
test('returns false if embeddable index patterns are empty', async () => {
|
||||
const { action, output, context } = setup();
|
||||
output.indexPatterns = [];
|
||||
const { action, embeddable, context } = setup();
|
||||
embeddable.dataViews = new BehaviorSubject<undefined | DataView[]>([]);
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
|
@ -159,8 +157,10 @@ describe('"Explore underlying data" panel action', () => {
|
|||
});
|
||||
|
||||
test('returns false if dashboard is in edit mode', async () => {
|
||||
const { action, input, context } = setup();
|
||||
input.viewMode = ViewMode.EDIT;
|
||||
const { action, embeddable, context } = setup();
|
||||
if (embeddable.parentApi) {
|
||||
embeddable.parentApi.viewMode = new BehaviorSubject<ViewModeType>(ViewMode.EDIT);
|
||||
}
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
|
@ -191,7 +191,8 @@ describe('"Explore underlying data" panel action', () => {
|
|||
|
||||
expect(locator.getLocation).toHaveBeenCalledTimes(1);
|
||||
expect(locator.getLocation).toHaveBeenCalledWith({
|
||||
indexPatternId: 'index-ptr-foo',
|
||||
dataViewId: 'index-ptr-foo',
|
||||
filters: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,23 +4,10 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import { EmbeddableContext, EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import { KibanaLocation } from '@kbn/share-plugin/public';
|
||||
import * as shared from './shared';
|
||||
import { AbstractExploreDataAction } from './abstract_explore_data_action';
|
||||
|
||||
interface EmbeddableQueryInput extends EmbeddableInput {
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
type EmbeddableQueryContext = EmbeddableContext<IEmbeddable<EmbeddableQueryInput>>;
|
||||
|
||||
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
||||
|
||||
/**
|
||||
|
@ -28,40 +15,12 @@ export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
|||
* menu of a dashboard panel.
|
||||
*/
|
||||
export class ExploreDataContextMenuAction
|
||||
extends AbstractExploreDataAction<EmbeddableQueryContext>
|
||||
implements Action<EmbeddableQueryContext>
|
||||
extends AbstractExploreDataAction
|
||||
implements Action<EmbeddableApiContext>
|
||||
{
|
||||
public readonly id = ACTION_EXPLORE_DATA;
|
||||
|
||||
public readonly type = ACTION_EXPLORE_DATA;
|
||||
|
||||
public readonly order = 200;
|
||||
|
||||
protected readonly getLocation = async (
|
||||
context: EmbeddableQueryContext
|
||||
): Promise<KibanaLocation> => {
|
||||
const { plugins } = this.params.start();
|
||||
const { locator } = plugins.discover;
|
||||
|
||||
if (!locator) {
|
||||
throw new Error('Discover URL locator not available.');
|
||||
}
|
||||
|
||||
const { embeddable } = context;
|
||||
const params: DiscoverAppLocatorParams = {};
|
||||
|
||||
if (embeddable) {
|
||||
params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
|
||||
|
||||
const input = embeddable.getInput();
|
||||
|
||||
if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange;
|
||||
if (input.query) params.query = input.query;
|
||||
if (input.filters) params.filters = [...input.filters, ...(params.filters || [])];
|
||||
}
|
||||
|
||||
const location = await locator.getLocation(params);
|
||||
|
||||
return location;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,21 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { apiPublishesDataViews, EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
|
||||
const isOutputWithIndexPatterns = (
|
||||
output: unknown
|
||||
): output is { indexPatterns: Array<{ id: string }> } => {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
return Array.isArray((output as any).indexPatterns);
|
||||
export const getDataViews = (embeddable: EmbeddableApiContext['embeddable']): string[] => {
|
||||
if (!apiPublishesDataViews(embeddable)) return [];
|
||||
|
||||
const dataViews: DataView[] = embeddable.dataViews.getValue() ?? [];
|
||||
return dataViews.reduce(
|
||||
(prev: string[], current: DataView) => (current.id ? [...prev, current.id] : prev),
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export const getIndexPatterns = (embeddable?: IEmbeddable): string[] => {
|
||||
if (!embeddable) return [];
|
||||
const output = embeddable.getOutput();
|
||||
|
||||
return isOutputWithIndexPatterns(output) ? output.indexPatterns.map(({ id }) => id) : [];
|
||||
};
|
||||
|
||||
export const hasExactlyOneIndexPattern = (embeddable?: IEmbeddable): boolean =>
|
||||
getIndexPatterns(embeddable).length === 1;
|
||||
export const hasExactlyOneDataView = (embeddable: EmbeddableApiContext['embeddable']): boolean =>
|
||||
getDataViews(embeddable).length === 1;
|
||||
|
|
|
@ -17,8 +17,10 @@
|
|||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/es-query",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/unified-search-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue