mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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:
parent
cbad3cbe4a
commit
4a2ba0efc6
18 changed files with 677 additions and 111 deletions
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
155
src/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
Normal file
155
src/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
Normal 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,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
.addPanelFlyout {
|
||||
width: 33vw;
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -7,7 +7,7 @@ import {
|
|||
|
||||
import {
|
||||
DashboardCloneModal,
|
||||
} from '../top_nav/clone_modal';
|
||||
} from './clone_modal';
|
||||
|
||||
let onClone;
|
||||
let onClose;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
209
src/ui/public/saved_objects/components/saved_object_finder.js
Normal file
209
src/ui/public/saved_objects/components/saved_object_finder.js
Normal 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,
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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('-')}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue