[Saved Search Embeddable] Add view action (#112396) (#113225)

* [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>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Maja Grubic 2021-09-28 12:02:16 +02:00 committed by GitHub
parent c43086eec7
commit 2299190672
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 234 additions and 3 deletions

View 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(),
};
};

View file

@ -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,

View file

@ -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}`,
});
});
});

View file

@ -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
);
}
}

View file

@ -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);

View file

@ -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'
);
});
});
}

View file

@ -130,6 +130,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('-')}`);