mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Saved Search Embeddable] Add view action (#112396)
* [Saved Search Embeddable] Add view action * Fix typescript and lint errors; add tests * Add a functional test * Fix a unit test * Renaming action Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ae4e7ccc51
commit
c0d68aac32
7 changed files with 234 additions and 3 deletions
33
src/plugins/discover/public/__mocks__/start_contract.ts
Normal file
33
src/plugins/discover/public/__mocks__/start_contract.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { ApplicationStart, PublicAppInfo } from 'src/core/public';
|
||||
import { deepFreeze } from '@kbn/std';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
|
||||
const capabilities = deepFreeze({
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
discover: {
|
||||
show: true,
|
||||
edit: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const createStartContractMock = (): jest.Mocked<ApplicationStart> => {
|
||||
const currentAppId$ = new Subject<string | undefined>();
|
||||
|
||||
return {
|
||||
applications$: new BehaviorSubject<Map<string, PublicAppInfo>>(new Map()),
|
||||
currentAppId$: currentAppId$.asObservable(),
|
||||
capabilities,
|
||||
navigateToApp: jest.fn(),
|
||||
navigateToUrl: jest.fn(),
|
||||
getUrlForApp: jest.fn(),
|
||||
};
|
||||
};
|
|
@ -216,7 +216,7 @@ export class SavedSearchEmbeddable
|
|||
if (!this.savedSearch.sort || !this.savedSearch.sort.length) {
|
||||
this.savedSearch.sort = getDefaultSort(
|
||||
indexPattern,
|
||||
getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc')
|
||||
this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -226,7 +226,7 @@ export class SavedSearchEmbeddable
|
|||
isLoading: false,
|
||||
sort: getDefaultSort(
|
||||
indexPattern,
|
||||
getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc')
|
||||
this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc')
|
||||
),
|
||||
rows: [],
|
||||
searchDescription: this.savedSearch.description,
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ContactCardEmbeddable } from 'src/plugins/embeddable/public/lib/test_samples';
|
||||
|
||||
import { ViewSavedSearchAction } from './view_saved_search_action';
|
||||
import { SavedSearchEmbeddable } from './saved_search_embeddable';
|
||||
import { createStartContractMock } from '../../__mocks__/start_contract';
|
||||
import { savedSearchMock } from '../../__mocks__/saved_search';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import { IndexPattern } from 'src/plugins/data/common';
|
||||
import { createFilterManagerMock } from 'src/plugins/data/public/query/filter_manager/filter_manager.mock';
|
||||
import { ViewMode } from 'src/plugins/embeddable/public';
|
||||
|
||||
const applicationMock = createStartContractMock();
|
||||
const savedSearch = savedSearchMock;
|
||||
const indexPatterns = [] as IndexPattern[];
|
||||
const services = discoverServiceMock;
|
||||
const filterManager = createFilterManagerMock();
|
||||
const searchInput = {
|
||||
timeRange: {
|
||||
from: '2021-09-15',
|
||||
to: '2021-09-16',
|
||||
},
|
||||
id: '1',
|
||||
viewMode: ViewMode.VIEW,
|
||||
};
|
||||
const executeTriggerActions = async (triggerId: string, context: object) => {
|
||||
return Promise.resolve(undefined);
|
||||
};
|
||||
const trigger = { id: 'ACTION_VIEW_SAVED_SEARCH' };
|
||||
const embeddableConfig = {
|
||||
savedSearch,
|
||||
editUrl: '',
|
||||
editPath: '',
|
||||
indexPatterns,
|
||||
editable: true,
|
||||
filterManager,
|
||||
services,
|
||||
};
|
||||
|
||||
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);
|
||||
const embeddable = new SavedSearchEmbeddable(
|
||||
embeddableConfig,
|
||||
searchInput,
|
||||
executeTriggerActions
|
||||
);
|
||||
expect(await action.isCompatible({ embeddable, trigger })).toBe(true);
|
||||
});
|
||||
|
||||
it('is not compatible when embeddable not of type saved search', async () => {
|
||||
const action = new ViewSavedSearchAction(applicationMock);
|
||||
const embeddable = new ContactCardEmbeddable(
|
||||
{
|
||||
id: '123',
|
||||
firstName: 'sue',
|
||||
viewMode: ViewMode.EDIT,
|
||||
},
|
||||
{
|
||||
execAction: () => Promise.resolve(undefined),
|
||||
}
|
||||
);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable,
|
||||
trigger,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is not visible when in edit mode', async () => {
|
||||
const action = new ViewSavedSearchAction(applicationMock);
|
||||
const input = { ...searchInput, viewMode: ViewMode.EDIT };
|
||||
const embeddable = new SavedSearchEmbeddable(embeddableConfig, input, executeTriggerActions);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable,
|
||||
trigger,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('execute navigates to a saved search', async () => {
|
||||
const action = new ViewSavedSearchAction(applicationMock);
|
||||
const embeddable = new SavedSearchEmbeddable(
|
||||
embeddableConfig,
|
||||
searchInput,
|
||||
executeTriggerActions
|
||||
);
|
||||
await action.execute({ embeddable, trigger });
|
||||
expect(applicationMock.navigateToApp).toHaveBeenCalledWith('discover', {
|
||||
path: `#/view/${savedSearch.id}`,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { ActionExecutionContext } from 'src/plugins/ui_actions/public';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IEmbeddable, ViewMode } from '../../../../embeddable/public';
|
||||
import { Action } from '../../../../ui_actions/public';
|
||||
import { SavedSearchEmbeddable } from './saved_search_embeddable';
|
||||
import { SEARCH_EMBEDDABLE_TYPE } from '../../../common';
|
||||
|
||||
export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH';
|
||||
|
||||
export interface ViewSearchContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class ViewSavedSearchAction implements Action<ViewSearchContext> {
|
||||
public id = ACTION_VIEW_SAVED_SEARCH;
|
||||
public readonly type = ACTION_VIEW_SAVED_SEARCH;
|
||||
|
||||
constructor(private readonly application: ApplicationStart) {}
|
||||
|
||||
async execute(context: ActionExecutionContext<ViewSearchContext>): Promise<void> {
|
||||
const { embeddable } = context;
|
||||
const savedSearchId = (embeddable as SavedSearchEmbeddable).getSavedSearch().id;
|
||||
const path = `#/view/${encodeURIComponent(savedSearchId)}`;
|
||||
const app = embeddable ? embeddable.getOutput().editApp : undefined;
|
||||
await this.application.navigateToApp(app ? app : 'discover', { path });
|
||||
}
|
||||
|
||||
getDisplayName(context: ActionExecutionContext<ViewSearchContext>): string {
|
||||
return i18n.translate('discover.savedSearchEmbeddable.action.viewSavedSearch.displayName', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
});
|
||||
}
|
||||
|
||||
getIconType(context: ActionExecutionContext<ViewSearchContext>): string | undefined {
|
||||
return 'inspect';
|
||||
}
|
||||
|
||||
async isCompatible(context: ActionExecutionContext<ViewSearchContext>) {
|
||||
const { embeddable } = context;
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ import { UsageCollectionSetup } from '../../usage_collection/public';
|
|||
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
|
||||
import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public';
|
||||
import { DeferredSpinner } from './shared';
|
||||
import { ViewSavedSearchAction } from './application/embeddable/view_saved_search_action';
|
||||
|
||||
declare module '../../share/public' {
|
||||
export interface UrlGeneratorStateMapping {
|
||||
|
@ -397,6 +398,10 @@ export class DiscoverPlugin
|
|||
// initializeServices are assigned at start and used
|
||||
// when the application/embeddable is mounted
|
||||
|
||||
const { uiActions } = plugins;
|
||||
|
||||
const viewSavedSearchAction = new ViewSavedSearchAction(core.application);
|
||||
uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
|
||||
setUiActions(plugins.uiActions);
|
||||
|
||||
const services = buildServices(core, plugins, this.initializerContext);
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const filterBar = getService('filterBar');
|
||||
const find = getService('find');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
@ -61,5 +62,33 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
.map((mark) => $(mark).text());
|
||||
expect(marks.length).to.be(0);
|
||||
});
|
||||
|
||||
it('view action leads to a saved search', async function () {
|
||||
await filterBar.removeAllFilters();
|
||||
await PageObjects.dashboard.saveDashboard('Dashboard With Saved Search');
|
||||
await PageObjects.dashboard.clickCancelOutOfEditMode(false);
|
||||
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
|
||||
expect(inViewMode).to.equal(true);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
const actionExists = await testSubjects.exists(
|
||||
'embeddablePanelAction-ACTION_VIEW_SAVED_SEARCH'
|
||||
);
|
||||
if (!actionExists) {
|
||||
await dashboardPanelActions.clickContextMenuMoreItem();
|
||||
}
|
||||
const actionElement = await testSubjects.find(
|
||||
'embeddablePanelAction-ACTION_VIEW_SAVED_SEARCH'
|
||||
);
|
||||
await actionElement.click();
|
||||
|
||||
await PageObjects.discover.waitForDiscoverAppOnScreen();
|
||||
expect(await PageObjects.discover.getSavedSearchTitle()).to.equal(
|
||||
'Rendering Test: saved search'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -123,6 +123,11 @@ export class DiscoverPageObject extends FtrService {
|
|||
return await searchLink.isDisplayed();
|
||||
}
|
||||
|
||||
public async getSavedSearchTitle() {
|
||||
const breadcrumb = await this.find.byCssSelector('[data-test-subj="breadcrumb last"]');
|
||||
return await breadcrumb.getVisibleText();
|
||||
}
|
||||
|
||||
public async loadSavedSearch(searchName: string) {
|
||||
await this.openLoadSavedSearchPanel();
|
||||
await this.testSubjects.click(`savedObjectTitle${searchName.split(' ').join('-')}`);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue