EUI dashboard add panel (#17374) (#19404)

* EUI add panel

* implement add functionallity

* style flyout so top nav is not covered

* add noItemsMessage

* add new visualization button

* remove angular add_panel template

* seperate search bar and table into its own component

* fix functional tests

* make slide out panel own focus to avoid weirdness of letting other buttons in nav from getting clicked and leaving slide out open

* remove deprecated method componentWillMount

* add jest test for DashboardAddPanel

* fix paging and replace EuiSearcBar with EuiFieldSearch

* fix functional tests

* fix dashboard filter bar functional test

* another functional test fix

* add more context to functional test failure message

* give search input a default value

* remove call to waitForRenderComplete to see if tests will pass

* fix dashboard filtering test

* updates from Stacey-Gammon review

* support filtering out lab visualizations

* add functional test for testing visualize:enableLabs with add panel

* add sorting by title to SavedObjectFinder componenet

* move add panel tabs to state

* clean up labs test differently
This commit is contained in:
Nathan Reese 2018-05-24 12:53:49 -06:00 committed by GitHub
parent cbad3cbe4a
commit 4a2ba0efc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 677 additions and 111 deletions

View file

@ -19,10 +19,13 @@ import { VisualizeConstants } from '../visualize/visualize_constants';
import { DashboardStateManager } from './dashboard_state_manager';
import { saveDashboard } from './lib';
import { showCloneModal } from './top_nav/show_clone_modal';
import { showAddPanel } from './top_nav/show_add_panel';
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
import * as filterActions from 'ui/doc_table/actions/filter';
import { FilterManagerProvider } from 'ui/filter_manager';
import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider';
@ -53,12 +56,14 @@ app.directive('dashboardApp', function ($injector) {
return {
restrict: 'E',
controllerAs: 'dashboardApp',
controller: function ($scope, $rootScope, $route, $routeParams, $location, getAppState, $compile, dashboardConfig, localStorage) {
controller: function ($scope, $rootScope, $route, $routeParams, $location, getAppState, dashboardConfig, localStorage) {
const filterManager = Private(FilterManagerProvider);
const filterBar = Private(FilterBarQueryFilterProvider);
const docTitle = Private(DocTitleProvider);
const notify = new Notifier({ location: 'Dashboard' });
const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider);
const savedObjectsClient = Private(SavedObjectsClientProvider);
const visTypes = Private(VisTypesRegistryProvider);
$scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType];
const dash = $scope.dash = $route.current.locals.dash;
@ -178,25 +183,6 @@ app.directive('dashboardApp', function ($injector) {
$scope.refresh();
};
// called by the saved-object-finder when a user clicks a vis
$scope.addVis = function (hit, showToast = true) {
dashboardStateManager.addNewPanel(hit.id, 'visualization');
if (showToast) {
toastNotifications.addSuccess({
title: 'Visualization was added to your dashboard',
'data-test-subj': 'addVisualizationToDashboardSuccess',
});
}
};
$scope.addSearch = function (hit) {
dashboardStateManager.addNewPanel(hit.id, 'search');
toastNotifications.addSuccess({
title: 'Saved search was added to your dashboard',
'data-test-subj': 'addSavedSearchToDashboardSuccess',
});
};
$scope.$watch('model.hidePanelTitles', () => {
dashboardStateManager.setHidePanelTitles($scope.model.hidePanelTitles);
});
@ -306,7 +292,7 @@ app.directive('dashboardApp', function ($injector) {
$scope.showAddPanel = () => {
dashboardStateManager.setFullScreenMode(false);
$scope.kbnTopNav.open('add');
$scope.kbnTopNav.click(TopNavIds.ADD);
};
$scope.enterEditMode = () => {
dashboardStateManager.setFullScreenMode(false);
@ -341,6 +327,19 @@ app.directive('dashboardApp', function ($injector) {
showCloneModal(onClone, currentTitle);
};
navActions[TopNavIds.ADD] = () => {
const addNewVis = () => {
kbnUrl.change(
`${VisualizeConstants.WIZARD_STEP_1_PAGE_PATH}?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`);
// Function is called outside of angular. Must apply digest cycle to trigger URL update
$scope.$apply();
};
const isLabsEnabled = config.get('visualize:enableLabs');
const listingLimit = config.get('savedObjects:listingLimit');
showAddPanel(savedObjectsClient, dashboardStateManager.addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes);
};
updateViewMode(dashboardStateManager.getViewMode());
// update root source when filters update
@ -375,27 +374,16 @@ app.directive('dashboardApp', function ($injector) {
}
if ($route.current.params && $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) {
// Hide the toast message since they will already see a notification from saving the visualization,
// and one is sufficient (especially given how the screen jumps down a bit for each unique notification).
const showToast = false;
$scope.addVis({ id: $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] }, showToast);
dashboardStateManager.addNewPanel($route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM], 'visualization');
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM);
}
const addNewVis = function addNewVis() {
kbnUrl.change(
`${VisualizeConstants.WIZARD_STEP_1_PAGE_PATH}?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`);
};
$scope.opts = {
displayName: dash.getDisplayName(),
dashboard: dash,
save: $scope.save,
addVis: $scope.addVis,
addNewVis,
addSearch: $scope.addSearch,
timefilter: $scope.timefilter
};
}

View file

@ -453,7 +453,7 @@ export class DashboardStateManager {
* @param {number} id
* @param {string} type
*/
addNewPanel(id, type) {
addNewPanel = (id, type) => {
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
const newPanel = createPanelState(id, type, maxPanelIndex, this.getPanels());
this.getPanels().push(newPanel);

View file

@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiFlyout
className="addPanelFlyout"
data-test-subj="dashboardAddPanel"
onClose={[Function]}
ownFocus={true}
size="s"
>
<EuiFlyoutBody>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiTitle
size="m"
>
<h2>
Add Panels
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonIcon
aria-label="close add panel"
color="primary"
data-test-subj="closeAddPanelBtn"
iconType="cross"
onClick={[Function]}
type="button"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTabs>
<EuiTab
data-test-subj="addVisualizationTab"
disabled={false}
isSelected={true}
key="vis"
onClick={[Function]}
>
Visualization
</EuiTab>
<EuiTab
data-test-subj="addSavedSearchTab"
disabled={false}
isSelected={false}
key="search"
onClick={[Function]}
>
Saved Search
</EuiTab>
</EuiTabs>
<EuiSpacer
size="s"
/>
<SavedObjectFinder
callToActionButton={
<EuiButton
color="primary"
data-test-subj="addNewSavedObjectLink"
fill={false}
iconSide="left"
onClick={[Function]}
type="button"
>
Add new Visualization
</EuiButton>
}
find={[Function]}
key="visSavedObjectFinder"
noItemsMessage="No matching visualizations found."
onChoose={[Function]}
savedObjectType="visualization"
/>
</EuiFlyoutBody>
</EuiFlyout>
`;

View file

@ -1,47 +0,0 @@
<div
ng-switch="mode"
ng-init="mode = 'visualization'"
>
<h2 class="kuiLocalDropdownTitle" data-test-subj="dashboardAddPanel">
Add Panels
</h2>
<div class="kuiTabs">
<button
ng-class="{ 'kuiTab-isSelected': mode == 'visualization'}"
class="kuiTab"
ng-click="mode='visualization'"
aria-label="List visualizations"
data-test-subj="addVisualizationTab"
>
Visualization
</button>
<button
ng-class="{ 'kuiTab-isSelected': mode == 'search' }"
class="kuiTab"
ng-click="mode='search'"
aria-label="List saved searches"
data-test-subj="addSavedSearchTab"
>
Saved Search
</button>
</div>
<div class="list-group-item list-group-item--noBorder" ng-switch-when="visualization">
<saved-object-finder
use-local-management="true"
type="visualizations"
on-add-new="opts.addNewVis"
on-choose="opts.addVis">
</saved-object-finder>
</div>
<div class="list-group-item list-group-item--noBorder" ng-switch-when="search">
<saved-object-finder
type="searches"
use-local-management="true"
on-choose="opts.addSearch"
></saved-object-finder>
</div>
</div>

View file

@ -0,0 +1,155 @@
import './add_panel.less';
import React from 'react';
import PropTypes from 'prop-types';
import { toastNotifications } from 'ui/notify';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiButton,
EuiButtonIcon,
EuiTabs,
EuiTab,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
const VIS_TAB_ID = 'vis';
const SAVED_SEARCH_TAB_ID = 'search';
export class DashboardAddPanel extends React.Component {
constructor(props) {
super(props);
const addNewVisBtn = (
<EuiButton
onClick={this.props.addNewVis}
data-test-subj="addNewSavedObjectLink"
>
Add new Visualization
</EuiButton>
);
const tabs = [{
id: VIS_TAB_ID,
name: 'Visualization',
dataTestSubj: 'addVisualizationTab',
toastDataTestSubj: 'addVisualizationToDashboardSuccess',
savedObjectFinder: (
<SavedObjectFinder
key="visSavedObjectFinder"
callToActionButton={addNewVisBtn}
onChoose={this.onAddPanel}
find={this.props.find}
noItemsMessage="No matching visualizations found."
savedObjectType="visualization"
/>
)
}, {
id: SAVED_SEARCH_TAB_ID,
name: 'Saved Search',
dataTestSubj: 'addSavedSearchTab',
toastDataTestSubj: 'addSavedSearchToDashboardSuccess',
savedObjectFinder: (
<SavedObjectFinder
key="searchSavedObjectFinder"
onChoose={this.onAddPanel}
find={this.props.find}
noItemsMessage="No matching saved searches found."
savedObjectType="search"
/>
)
}];
this.state = {
tabs: tabs,
selectedTab: tabs[0],
};
}
onSelectedTabChanged = tab => {
this.setState({
selectedTab: tab,
});
}
renderTabs() {
return this.state.tabs.map((tab) => {
return (
<EuiTab
onClick={() => this.onSelectedTabChanged(tab)}
isSelected={tab.id === this.state.selectedTab.id}
key={tab.id}
data-test-subj={tab.dataTestSubj}
>
{tab.name}
</EuiTab>
);
});
}
onAddPanel = (id, type) => {
this.props.addNewPanel(id, type);
// To avoid the clutter of having toast messages cover flyout
// close previous toast message before creating a new one
if (this.lastToast) {
toastNotifications.remove(this.lastToast);
}
this.lastToast = toastNotifications.addSuccess({
title: `${this.state.selectedTab.name} was added to your dashboard`,
'data-test-subj': this.state.selectedTab.toastDataTestSubj,
});
}
render() {
return (
<EuiFlyout
ownFocus
className="addPanelFlyout"
onClose={this.props.onClose}
size="s"
data-test-subj="dashboardAddPanel"
>
<EuiFlyoutBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle>
<h2>Add Panels</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
onClick={this.props.onClose}
aria-label="close add panel"
data-test-subj="closeAddPanelBtn"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTabs>
{this.renderTabs()}
</EuiTabs>
<EuiSpacer size="s" />
{this.state.selectedTab.savedObjectFinder}
</EuiFlyoutBody>
</EuiFlyout>
);
}
}
DashboardAddPanel.propTypes = {
onClose: PropTypes.func.isRequired,
find: PropTypes.func.isRequired,
addNewPanel: PropTypes.func.isRequired,
addNewVis: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,3 @@
.addPanelFlyout {
width: 33vw;
}

View file

@ -0,0 +1,43 @@
import React from 'react';
import sinon from 'sinon';
import { mount, shallow } from 'enzyme';
import {
findTestSubject,
} from '@elastic/eui/lib/test';
import {
DashboardAddPanel,
} from './add_panel';
jest.mock('ui/notify',
() => ({
toastNotifications: {
addDanger: () => {},
}
}), { virtual: true });
let onClose;
beforeEach(() => {
onClose = sinon.spy();
});
test('render', () => {
const component = shallow(<DashboardAddPanel
onClose={onClose}
find={() => {}}
addNewPanel={() => {}}
addNewVis={() => {}}
/>);
expect(component).toMatchSnapshot();
});
test('onClose', () => {
const component = mount(<DashboardAddPanel
onClose={onClose}
find={() => {}}
addNewPanel={() => {}}
addNewVis={() => {}}
/>);
findTestSubject(component, 'closeAddPanelBtn', false).simulate('click');
sinon.assert.calledOnce(onClose);
});

View file

@ -7,7 +7,7 @@ import {
import {
DashboardCloneModal,
} from '../top_nav/clone_modal';
} from './clone_modal';
let onClone;
let onClose;

View file

@ -28,7 +28,7 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
return [
getSaveConfig(),
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
getAddConfig(),
getAddConfig(actions[TopNavIds.ADD]),
getOptionsConfig(),
getShareConfig()];
default:
@ -96,12 +96,12 @@ function getCloneConfig(action) {
/**
* @returns {kbnTopNavConfig}
*/
function getAddConfig() {
function getAddConfig(action) {
return {
key: TopNavIds.ADD,
description: 'Add a panel to the dashboard',
testId: 'dashboardAddPanelButton',
template: require('plugins/kibana/dashboard/top_nav/add_panel.html')
run: action
};
}

View file

@ -0,0 +1,55 @@
import { DashboardAddPanel } from './add_panel';
import React from 'react';
import ReactDOM from 'react-dom';
let isOpen = false;
export function showAddPanel(savedObjectsClient, addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes) {
if (isOpen) {
return;
}
isOpen = true;
const container = document.createElement('div');
const onClose = () => {
ReactDOM.unmountComponentAtNode(container);
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();
addNewVis();
};
document.body.appendChild(container);
const element = (
<DashboardAddPanel
onClose={onClose}
find={find}
addNewPanel={addNewPanel}
addNewVis={addNewVisWithCleanup}
/>
);
ReactDOM.render(element, container);
}

View file

@ -0,0 +1,209 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiFieldSearch,
EuiBasicTable,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
export class SavedObjectFinder extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
isFetchingItems: false,
page: 0,
perPage: 10,
filter: '',
};
}
componentWillUnmount() {
this._isMounted = false;
this.debouncedFetch.cancel();
}
componentDidMount() {
this._isMounted = true;
this.fetchItems();
}
onTableChange = ({ page, sort = {} }) => {
let {
field: sortField,
direction: sortDirection,
} = sort;
// 3rd sorting state that is not captured by sort - native order (no sort)
// when switching from desc to asc for the same field - use native order
if (this.state.sortField === sortField
&& this.state.sortDirection === 'desc'
&& sortDirection === 'asc') {
sortField = null;
sortDirection = null;
}
this.setState({
page: page.index,
perPage: page.size,
sortField,
sortDirection,
});
}
// server-side paging not supported
// 1) saved object client does not support sorting by title because title is only mapped as analyzed
// 2) can not search on anything other than title because all other fields are stored in opaque JSON strings,
// for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side
// with the current mappings
getPageOfItems = () => {
// do not sort original list to preserve elasticsearch ranking order
const items = this.state.items.slice();
if (this.state.sortField) {
items.sort((a, b) => {
const fieldA = _.get(a, this.state.sortField, '');
const fieldB = _.get(b, this.state.sortField, '');
let order = 1;
if (this.state.sortDirection === 'desc') {
order = -1;
}
return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase());
});
}
// If begin is greater than the length of the sequence, an empty array is returned.
const startIndex = this.state.page * this.state.perPage;
// If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length).
const lastIndex = startIndex + this.state.perPage;
return items.slice(startIndex, lastIndex);
}
debouncedFetch = _.debounce(async (filter) => {
const response = await this.props.find(this.props.savedObjectType, filter);
if (!this._isMounted) {
return;
}
// We need this check to handle the case where search results come back in a different
// order than they were sent out. Only load results for the most recent search.
if (filter === this.state.filter) {
this.setState({
isFetchingItems: false,
items: response.savedObjects.map(savedObject => {
return {
title: savedObject.attributes.title,
id: savedObject.id,
type: savedObject.type,
};
}),
});
}
}, 300);
fetchItems = () => {
this.setState({
isFetchingItems: true,
}, this.debouncedFetch.bind(null, this.state.filter));
}
renderSearchBar() {
let actionBtn;
if (this.props.callToActionButton) {
actionBtn = (
<EuiFlexItem grow={false}>
{this.props.callToActionButton}
</EuiFlexItem>
);
}
return (
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<EuiFieldSearch
placeholder="Search..."
fullWidth
value={this.state.filter}
onChange={(e) => {
this.setState({
filter: e.target.value
}, this.fetchItems);
}}
data-test-subj="savedObjectFinderSearchInput"
/>
</EuiFlexItem>
{actionBtn}
</EuiFlexGroup>
);
}
renderTable() {
const pagination = {
pageIndex: this.state.page,
pageSize: this.state.perPage,
totalItemCount: this.state.items.length,
pageSizeOptions: [5, 10],
};
const sorting = {};
if (this.state.sortField) {
sorting.sort = {
field: this.state.sortField,
direction: this.state.sortDirection,
};
}
const tableColumns = [
{
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>
)
}
];
const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
return (
<EuiBasicTable
items={items}
loading={this.state.isFetchingItems}
columns={tableColumns}
pagination={pagination}
sorting={sorting}
onChange={this.onTableChange}
noItemsMessage={this.props.noItemsMessage}
/>
);
}
render() {
return (
<React.Fragment>
{this.renderSearchBar()}
{this.renderTable()}
</React.Fragment>
);
}
}
SavedObjectFinder.propTypes = {
callToActionButton: PropTypes.node,
onChoose: PropTypes.func.isRequired,
find: PropTypes.func.isRequired,
noItemsMessage: PropTypes.node,
savedObjectType: PropTypes.oneOf(['visualization', 'search']).isRequired,
};

View file

@ -6,7 +6,7 @@ import {
export default function ({ getService, getPageObjects }) {
const retry = getService('retry');
const PageObjects = getPageObjects(['dashboard', 'header', 'visualize']);
const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']);
const remote = getService('remote');
const dashboardAddPanel = getService('dashboardAddPanel');
@ -41,6 +41,42 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.clickDashboard();
});
});
describe('visualize:enableLabs advanced setting', () => {
const LAB_VIS_NAME = 'Rendering Test: input control';
it('should display lab visualizations in add panel', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
const exists = await dashboardAddPanel.panelAddLinkExists(LAB_VIS_NAME);
await dashboardAddPanel.closeAddPanel();
expect(exists).to.be(true);
});
describe('is false', () => {
before(async () => {
await PageObjects.header.clickManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs');
});
it('should not display lab visualizations in add panel', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
const exists = await dashboardAddPanel.panelAddLinkExists(LAB_VIS_NAME);
await dashboardAddPanel.closeAddPanel();
expect(exists).to.be(false);
});
after(async () => {
await PageObjects.header.clickManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs');
await PageObjects.header.clickDashboard();
});
});
});
});
}

View file

@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) {
});
it ('should continue to show for visualizations with no search source', async () => {
await dashboardAddPanel.addVisualization('input control');
await dashboardAddPanel.addVisualization('Rendering-Test:-input-control');
const hasAddFilter = await testSubjects.exists('addFilter');
expect(hasAddFilter).to.be(true);
});
@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }) {
});
it('shows index pattern of vis when one is added', async () => {
await dashboardAddPanel.addVisualization('animal sounds pie');
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await PageObjects.header.waitUntilLoadingHasFinished();
await filterBar.ensureFieldEditorModalIsClosed();
await testSubjects.click('addFilter');
@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }) {
});
it('works when a vis with no index pattern is added', async () => {
await dashboardAddPanel.addVisualization('markdown');
await dashboardAddPanel.addVisualization('Rendering-Test:-markdown');
await PageObjects.header.waitUntilLoadingHasFinished();
await filterBar.ensureFieldEditorModalIsClosed();
await testSubjects.click('addFilter');

View file

@ -148,7 +148,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.setTimepickerIn63DataRange();
await dashboardAddPanel.addVisualization('animal sounds pie');
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
await dashboardExpect.pieSliceCount(5);
@ -160,7 +160,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.waitForRenderComplete();
await dashboardExpect.pieSliceCount(3);
await PageObjects.visualize.saveVisualization('animal sounds pie');
await PageObjects.visualize.saveVisualization('Rendering-Test:-animal-sounds-pie');
await PageObjects.header.clickDashboard();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();

View file

@ -36,8 +36,6 @@ export default function ({ getService, getPageObjects }) {
const panelCount = await PageObjects.dashboard.getPanelCount();
expect(panelCount).to.be(27);
await PageObjects.dashboard.waitForRenderComplete();
// Not neccessary but helpful for local debugging.
await PageObjects.dashboard.saveDashboard('embeddable rendering test');
});

View file

@ -283,6 +283,18 @@ export function CommonPageProvider({ getService, getPageObjects }) {
}
});
}
async clearAllToasts() {
const toasts = await find.allByCssSelector('.euiToast');
for (const toastElement of toasts) {
try {
const closeBtn = await toastElement.findByCssSelector('euiToast__closeButton');
await closeBtn.click();
} catch (err) {
// ignore errors, toast clear themselves after timeout
}
}
}
}
return new CommonPage();

View file

@ -611,11 +611,14 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
return await sharedItem.getAttribute('data-render-complete');
}));
if (renderComplete.length !== sharedItems.length) {
throw new Error('Some shared items dont have data-render-complete attribute');
const expecting = `expecting: ${sharedItems.length}, received: ${renderComplete.length}`;
throw new Error(
`Some shared items dont have data-render-complete attribute, ${expecting}`);
}
const totalCount = renderComplete.filter(value => value === 'true' || value === 'disabled').length;
if (totalCount < sharedItems.length) {
throw new Error('Still waiting on more visualizations to finish rendering');
const expecting = `${sharedItems.length}, received: ${totalCount}`;
throw new Error(`Still waiting on more visualizations to finish rendering, expecting: ${expecting}`);
}
});
}

View file

@ -3,8 +3,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
const log = getService('log');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const find = getService('find');
const PageObjects = getPageObjects(['header']);
const PageObjects = getPageObjects(['header', 'common']);
return new class DashboardAddPanel {
async clickOpenAddPanel() {
@ -16,18 +15,14 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await testSubjects.click('addNewSavedObjectLink');
}
async closeAddVizualizationPanel() {
log.debug('closeAddVizualizationPanel');
await find.clickByCssSelector('i.fa fa-chevron-up');
}
async clickSavedSearchTab() {
await testSubjects.click('addSavedSearchTab');
}
async addEveryEmbeddableOnCurrentPage() {
log.debug('addEveryEmbeddableOnCurrentPage');
const embeddableRows = await find.allByCssSelector('.list-group-menu-item');
const addPanel = await testSubjects.find('dashboardAddPanel');
const embeddableRows = await addPanel.findAllByClassName('euiLink');
for (let i = 0; i < embeddableRows.length; i++) {
await embeddableRows[i].click();
}
@ -35,12 +30,28 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
}
async clickPagerNextButton() {
const pagerNextButtonExists = await testSubjects.exists('paginateNext');
if (pagerNextButtonExists) {
await testSubjects.click('paginateNext');
await PageObjects.header.waitUntilLoadingHasFinished();
// Clear all toasts that could hide pagination controls
await PageObjects.common.clearAllToasts();
const addPanel = await testSubjects.find('dashboardAddPanel');
const pagination = await addPanel.findAllByClassName('euiPagination');
if (pagination.length === 0) {
return false;
}
return pagerNextButtonExists;
const pagerNextButton = await pagination[0].findByCssSelector('button[aria-label="Next page"]');
if (!pagerNextButton) {
return false;
}
const isDisabled = await pagerNextButton.getAttribute('disabled');
if (isDisabled != null) {
return false;
}
await pagerNextButton.click();
await PageObjects.header.waitUntilLoadingHasFinished();
return true;
}
async isAddPanelOpen() {
@ -67,7 +78,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
const isOpen = await this.isAddPanelOpen();
if (isOpen) {
await retry.try(async () => {
await this.clickOpenAddPanel();
await testSubjects.click('closeAddPanelBtn');
const isOpen = await this.isAddPanelOpen();
if (isOpen) {
throw new Error('Add panel still open, trying again.');
@ -87,6 +98,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await this.addEveryEmbeddableOnCurrentPage();
morePages = await this.clickPagerNextButton();
}
await this.closeAddPanel();
}
async addEverySavedSearch(filter) {
@ -101,6 +113,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await this.addEveryEmbeddableOnCurrentPage();
morePages = await this.clickPagerNextButton();
}
await this.closeAddPanel();
}
async addSavedSearch(searchName) {
@ -110,7 +123,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await this.clickSavedSearchTab();
await this.filterEmbeddableNames(searchName);
await find.clickByPartialLinkText(searchName);
await testSubjects.click(`addPanel${searchName.split(' ').join('-')}`);
await testSubjects.exists('addSavedSearchToDashboardSuccess');
await this.closeAddPanel();
}
@ -132,7 +145,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
log.debug(`DashboardAddPanel.addVisualization(${vizName})`);
await this.ensureAddPanelIsShowing();
await this.filterEmbeddableNames(`"${vizName.replace('-', ' ')}"`);
await find.clickByPartialLinkText(vizName);
await testSubjects.click(`addPanel${vizName.split(' ').join('-')}`);
await this.closeAddPanel();
}
@ -140,5 +153,12 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await testSubjects.setValue('savedObjectFinderSearchInput', name);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async panelAddLinkExists(name) {
log.debug(`DashboardAddPanel.panelAddLinkExists(${name})`);
await this.ensureAddPanelIsShowing();
await this.filterEmbeddableNames(`"${name}"`);
return await testSubjects.exists(`addPanel${name.split(' ').join('-')}`);
}
};
}