Skip to content

Commit

Permalink
[Saved Search Embeddable] Add view action (#112396)
Browse files Browse the repository at this point in the history
* [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>
  • Loading branch information
Maja Grubic and kibanamachine authored Sep 27, 2021
1 parent ae4e7cc commit c0d68aa
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 3 deletions.
33 changes: 33 additions & 0 deletions src/plugins/discover/public/__mocks__/start_contract.ts
Original file line number Diff line number Diff line change
@@ -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(),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
}

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`,
});
});
});
Original file line number Diff line number Diff line change
@@ -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
);
}
}
5 changes: 5 additions & 0 deletions src/plugins/discover/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
31 changes: 30 additions & 1 deletion test/functional/apps/dashboard/saved_search_embeddable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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'
);
});
});
}
5 changes: 5 additions & 0 deletions test/functional/page_objects/discover_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('-')}`);
Expand Down

0 comments on commit c0d68aa

Please sign in to comment.