Convert Discover open top nav to EUI flyout (#22971) (#23376)

* move find logic to SavedObjectFinder component since savedObjectClient is no longer coupled to angular

* implement flyout open saved searches

* remove old open stuff

* add jest test for OpenSearchPanel and simplify panel title

* fix functional tests

* fix _lab_mode functional test
This commit is contained in:
Nathan Reese 2018-09-20 15:05:17 -06:00 committed by GitHub
parent 2971d7635e
commit f750ab71c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 297 additions and 92 deletions

View file

@ -372,10 +372,7 @@ app.directive('dashboardApp', function ($injector) {
$scope.$apply();
};
const isLabsEnabled = config.get('visualize:enableLabs');
const listingLimit = config.get('savedObjects:listingLimit');
showAddPanel(chrome.getSavedObjectsClient(), dashboardStateManager.addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes);
showAddPanel(dashboardStateManager.addNewPanel, addNewVis, visTypes);
};
navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => {
showOptionsPopover({

View file

@ -11,28 +11,13 @@ exports[`render 1`] = `
size="s"
>
<EuiFlyoutBody>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
<EuiTitle
size="s"
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiTitle
size="m"
>
<h2>
Add Panels
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<h1>
Add Panels
</h1>
</EuiTitle>
<EuiTabs
expand={false}
size="m"
@ -72,11 +57,11 @@ exports[`render 1`] = `
Add new Visualization
</EuiButton>
}
find={[Function]}
key="visSavedObjectFinder"
noItemsMessage="No matching visualizations found."
onChoose={[Function]}
savedObjectType="visualization"
visTypes={Object {}}
/>
</EuiFlyoutBody>
</EuiFlyout>

View file

@ -23,8 +23,6 @@ import { toastNotifications } from 'ui/notify';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiButton,
@ -60,7 +58,7 @@ export class DashboardAddPanel extends React.Component {
key="visSavedObjectFinder"
callToActionButton={addNewVisBtn}
onChoose={this.onAddPanel}
find={this.props.find}
visTypes={this.props.visTypes}
noItemsMessage="No matching visualizations found."
savedObjectType="visualization"
/>
@ -74,7 +72,6 @@ export class DashboardAddPanel extends React.Component {
<SavedObjectFinder
key="searchSavedObjectFinder"
onChoose={this.onAddPanel}
find={this.props.find}
noItemsMessage="No matching saved searches found."
savedObjectType="search"
/>
@ -133,13 +130,9 @@ export class DashboardAddPanel extends React.Component {
>
<EuiFlyoutBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle>
<h2>Add Panels</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTitle size="s">
<h1>Add Panels</h1>
</EuiTitle>
<EuiTabs>
{this.renderTabs()}
@ -157,7 +150,7 @@ export class DashboardAddPanel extends React.Component {
DashboardAddPanel.propTypes = {
onClose: PropTypes.func.isRequired,
find: PropTypes.func.isRequired,
visTypes: PropTypes.object.isRequired,
addNewPanel: PropTypes.func.isRequired,
addNewVis: PropTypes.func.isRequired,
};

View file

@ -40,7 +40,7 @@ beforeEach(() => {
test('render', () => {
const component = shallow(<DashboardAddPanel
onClose={onClose}
find={() => {}}
visTypes={{}}
addNewPanel={() => {}}
addNewVis={() => {}}
/>);

View file

@ -23,7 +23,7 @@ import ReactDOM from 'react-dom';
let isOpen = false;
export function showAddPanel(savedObjectsClient, addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes) {
export function showAddPanel(addNewPanel, addNewVis, visTypes) {
if (isOpen) {
return;
}
@ -35,26 +35,6 @@ export function showAddPanel(savedObjectsClient, addNewPanel, addNewVis, listing
document.body.removeChild(container);
isOpen = false;
};
const find = async (type, search) => {
const resp = await savedObjectsClient.find({
type: type,
fields: ['title', 'visState'],
search: search ? `${search}*` : undefined,
page: 1,
perPage: listingLimit,
searchFields: ['title^3', 'description']
});
if (type === 'visualization' && !isLabsEnabled) {
resp.savedObjects = resp.savedObjects.filter(savedObject => {
const typeName = JSON.parse(savedObject.attributes.visState).type;
const visType = visTypes.byName[typeName];
return visType.stage !== 'lab';
});
}
return resp;
};
const addNewVisWithCleanup = () => {
onClose();
@ -65,7 +45,7 @@ export function showAddPanel(savedObjectsClient, addNewPanel, addNewVis, listing
const element = (
<DashboardAddPanel
onClose={onClose}
find={find}
visTypes={visTypes}
addNewPanel={addNewPanel}
addNewVis={addNewVisWithCleanup}
/>

View file

@ -58,6 +58,7 @@ import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
import { Inspector } from 'ui/inspector';
import { RequestAdapter } from 'ui/inspector/adapters';
import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils';
import { showOpenSearchPanel } from '../top_nav/show_open_search_panel';
import { tabifyAggResponse } from 'ui/agg_response/tabify';
const app = uiModules.get('apps/discover', [
@ -198,8 +199,14 @@ function discoverController(
}, {
key: 'open',
description: 'Open Saved Search',
template: require('plugins/kibana/discover/partials/load_search.html'),
testId: 'discoverOpenButton',
run: () => {
showOpenSearchPanel({
makeUrl: (searchId) => {
return kbnUrl.eval('#/discover/{{id}}', { id: searchId });
}
});
}
}, {
key: 'share',
description: 'Share Search',

View file

@ -1,7 +0,0 @@
<form role="form" ng-submit="fetch()" data-test-subj="loadSearchForm">
<h2 class="kuiLocalDropdownTitle">
Open Search
</h2>
<saved-object-finder type="searches"></saved-object-finder>
</form>

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
data-test-subj="loadSearchForm"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={true}
size="m"
>
<EuiFlyoutBody>
<EuiTitle
size="s"
>
<h1>
Open Search
</h1>
</EuiTitle>
<EuiSpacer
size="m"
/>
<SavedObjectFinder
callToActionButton={
<EuiButton
color="primary"
fill={false}
href="#/management/kibana/objects?_a=(tab:search)"
iconSide="left"
onClick={[Function]}
type="button"
>
Manage searches
</EuiButton>
}
makeUrl={[Function]}
noItemsMessage="No matching searches found."
onChoose={[Function]}
savedObjectType="search"
/>
</EuiFlyoutBody>
</EuiFlyout>
`;

View file

@ -0,0 +1,80 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import rison from 'rison-node';
import {
EuiSpacer,
EuiFlyout,
EuiFlyoutBody,
EuiTitle,
EuiButton,
} from '@elastic/eui';
const SEARCH_OBJECT_TYPE = 'search';
export class OpenSearchPanel extends React.Component {
renderMangageSearchesButton() {
return (
<EuiButton
onClick={this.props.onClose}
href={`#/management/kibana/objects?_a=${rison.encode({ tab: SEARCH_OBJECT_TYPE })}`}
>
Manage searches
</EuiButton>
);
}
render() {
return (
<EuiFlyout
ownFocus
onClose={this.props.onClose}
data-test-subj="loadSearchForm"
>
<EuiFlyoutBody>
<EuiTitle size="s">
<h1>Open Search</h1>
</EuiTitle>
<EuiSpacer size="m" />
<SavedObjectFinder
noItemsMessage="No matching searches found."
savedObjectType={SEARCH_OBJECT_TYPE}
makeUrl={this.props.makeUrl}
onChoose={this.props.onClose}
callToActionButton={this.renderMangageSearchesButton()}
/>
</EuiFlyoutBody>
</EuiFlyout>
);
}
}
OpenSearchPanel.propTypes = {
onClose: PropTypes.func.isRequired,
makeUrl: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,33 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import {
OpenSearchPanel,
} from './open_search_panel';
test('render', () => {
const component = shallow(<OpenSearchPanel
onClose={() => {}}
makeUrl={() => {}}
/>);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { OpenSearchPanel } from './open_search_panel';
let isOpen = false;
export function showOpenSearchPanel({ makeUrl }) {
if (isOpen) {
return;
}
isOpen = true;
const container = document.createElement('div');
const onClose = () => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
isOpen = false;
};
document.body.appendChild(container);
const element = (
<OpenSearchPanel
onClose={onClose}
makeUrl={makeUrl}
/>
);
ReactDOM.render(element, container);
}

View file

@ -20,6 +20,7 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import chrome from 'ui/chrome';
import {
EuiFieldSearch,
@ -104,7 +105,24 @@ export class SavedObjectFinder extends React.Component {
}
debouncedFetch = _.debounce(async (filter) => {
const response = await this.props.find(this.props.savedObjectType, filter);
const resp = await chrome.getSavedObjectsClient().find({
type: this.props.savedObjectType,
fields: ['title', 'visState'],
search: filter ? `${filter}*` : undefined,
page: 1,
perPage: chrome.getUiSettingsClient().get('savedObjects:listingLimit'),
searchFields: ['title^3', 'description']
});
if (this.props.savedObjectType === 'visualization'
&& !chrome.getUiSettingsClient().get('visualize:enableLabs')
&& this.props.visTypes) {
resp.savedObjects = resp.savedObjects.filter(savedObject => {
const typeName = JSON.parse(savedObject.attributes.visState).type;
const visType = this.props.visTypes.byName[typeName];
return visType.stage !== 'lab';
});
}
if (!this._isMounted) {
return;
@ -115,7 +133,7 @@ export class SavedObjectFinder extends React.Component {
if (filter === this.state.filter) {
this.setState({
isFetchingItems: false,
items: response.savedObjects.map(savedObject => {
items: resp.savedObjects.map(savedObject => {
return {
title: savedObject.attributes.title,
id: savedObject.id,
@ -183,16 +201,26 @@ export class SavedObjectFinder extends React.Component {
field: 'title',
name: 'Title',
sortable: true,
render: (field, record) => (
<EuiLink
onClick={() => {
this.props.onChoose(record.id, record.type);
}}
data-test-subj={`addPanel${field.split(' ').join('-')}`}
>
{field}
</EuiLink>
)
render: (title, record) => {
const {
onChoose,
makeUrl
} = this.props;
if (!onChoose && !makeUrl) {
return <span>{title}</span>;
}
return (
<EuiLink
onClick={onChoose ? () => { onChoose(record.id, record.type); } : undefined}
href={makeUrl ? makeUrl(record.id) : undefined}
data-test-subj={`savedObjectTitle${title.split(' ').join('-')}`}
>
{title}
</EuiLink>
);
}
}
];
const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
@ -221,8 +249,9 @@ export class SavedObjectFinder extends React.Component {
SavedObjectFinder.propTypes = {
callToActionButton: PropTypes.node,
onChoose: PropTypes.func.isRequired,
find: PropTypes.func.isRequired,
onChoose: PropTypes.func,
makeUrl: PropTypes.func,
noItemsMessage: PropTypes.node,
savedObjectType: PropTypes.oneOf(['visualization', 'search']).isRequired,
visTypes: PropTypes.object,
};

View file

@ -29,9 +29,10 @@ export default function ({ getService, getPageObjects }) {
it('disabling does not break loading saved searches', async () => {
await PageObjects.common.navigateToUrl('discover', '');
await PageObjects.discover.saveSearch('visualize_lab_mode_test');
await PageObjects.discover.openSavedSearch();
await PageObjects.discover.openLoadSavedSearchPanel();
const hasSaved = await PageObjects.discover.hasSavedSearch('visualize_lab_mode_test');
expect(hasSaved).to.be(true);
await PageObjects.discover.closeLoadSaveSearchPanel();
log.info('found saved search before toggling enableLabs mode');
@ -42,13 +43,14 @@ export default function ({ getService, getPageObjects }) {
// Expect the discover still to list that saved visualization in the open list
await PageObjects.header.clickDiscover();
await PageObjects.discover.openSavedSearch();
await PageObjects.discover.openLoadSavedSearchPanel();
const stillHasSaved = await PageObjects.discover.hasSavedSearch('visualize_lab_mode_test');
expect(stillHasSaved).to.be(true);
log.info('found saved search after toggling enableLabs mode');
});
after(async () => {
await PageObjects.discover.closeLoadSaveSearchPanel();
await PageObjects.header.clickManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs');

View file

@ -25,6 +25,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const find = getService('find');
const flyout = getService('flyout');
const PageObjects = getPageObjects(['header', 'common']);
const getRemote = () => (
@ -71,24 +72,38 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
return await Promise.all(headerElements.map(el => el.getVisibleText()));
}
async openSavedSearch() {
async openLoadSavedSearchPanel() {
const isOpen = await testSubjects.exists('loadSearchForm');
if (isOpen) {
return;
}
// We need this try loop here because previous actions in Discover like
// saving a search cause reloading of the page and the "Open" menu item goes stale.
await retry.try(async () => {
await this.clickLoadSavedSearchButton();
await PageObjects.header.waitUntilLoadingHasFinished();
const loadIsOpen = await testSubjects.exists('loadSearchForm');
expect(loadIsOpen).to.be(true);
const isOpen = await testSubjects.exists('loadSearchForm');
expect(isOpen).to.be(true);
});
}
async closeLoadSaveSearchPanel() {
const isOpen = await testSubjects.exists('loadSearchForm');
if (!isOpen) {
return;
}
await flyout.close('loadSearchForm');
}
async hasSavedSearch(searchName) {
const searchLink = await find.byPartialLinkText(searchName);
return searchLink.isDisplayed();
}
async loadSavedSearch(searchName) {
await this.clickLoadSavedSearchButton();
await this.openLoadSavedSearchPanel();
const searchLink = await find.byPartialLinkText(searchName);
await searchLink.click();
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -151,7 +151,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await this.clickSavedSearchTab();
await this.filterEmbeddableNames(searchName);
await testSubjects.click(`addPanel${searchName.split(' ').join('-')}`);
await testSubjects.click(`savedObjectTitle${searchName.split(' ').join('-')}`);
await testSubjects.exists('addSavedSearchToDashboardSuccess');
await this.closeAddPanel();
}
@ -173,7 +173,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
log.debug(`DashboardAddPanel.addVisualization(${vizName})`);
await this.ensureAddPanelIsShowing();
await this.filterEmbeddableNames(`"${vizName.replace('-', ' ')}"`);
await testSubjects.click(`addPanel${vizName.split(' ').join('-')}`);
await testSubjects.click(`savedObjectTitle${vizName.split(' ').join('-')}`);
await this.closeAddPanel();
}
@ -188,7 +188,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
log.debug(`DashboardAddPanel.panelAddLinkExists(${name})`);
await this.ensureAddPanelIsShowing();
await this.filterEmbeddableNames(`"${name}"`);
return await testSubjects.exists(`addPanel${name.split(' ').join('-')}`);
return await testSubjects.exists(`savedObjectTitle${name.split(' ').join('-')}`);
}
};
}