diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js
index 880455cbd82d7..67ac47d9b1ace 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js
@@ -478,7 +478,7 @@ app.directive('dashboardApp', function ($injector) {
showNewVisModal(visTypes, { editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM] });
};
- showAddPanel(dashboardStateManager.addNewPanel, addNewVis, visTypes);
+ showAddPanel(dashboardStateManager.addNewPanel, addNewVis, embeddableFactories);
};
navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => {
showOptionsPopover({
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap
index 31e5edc76f1c2..32311d82587c4 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap
@@ -10,68 +10,60 @@ exports[`render 1`] = `
ownFocus={true}
size="m"
>
-
+
-
+
-
+
-
+
+
+
+
+
-
- Visualization
-
-
- Saved Search
-
-
-
-
- }
- key="visSavedObjectFinder"
- noItemsMessage="No matching visualizations found."
- onChoose={[Function]}
- savedObjectType="visualization"
- visTypes={Object {}}
- />
-
+
+
+
`;
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
index e9f4709c7e927..febf53de9670a 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
@@ -19,109 +19,24 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import { toastNotifications } from 'ui/notify';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutFooter,
EuiFlyoutBody,
EuiButton,
- EuiTabs,
- EuiTab,
- EuiSpacer,
EuiTitle,
} from '@elastic/eui';
-const VIS_TAB_ID = 'vis';
-const SAVED_SEARCH_TAB_ID = 'search';
-
-class DashboardAddPanelUi extends React.Component {
- constructor(props) {
- super(props);
-
- const addNewVisBtn = (
-
-
-
- );
-
- const tabs = [{
- id: VIS_TAB_ID,
- name: props.intl.formatMessage({
- id: 'kbn.dashboard.topNav.addPanel.visualizationTabName',
- defaultMessage: 'Visualization',
- }),
- dataTestSubj: 'addVisualizationTab',
- toastDataTestSubj: 'addVisualizationToDashboardSuccess',
- savedObjectFinder: (
-
- )
- }, {
- id: SAVED_SEARCH_TAB_ID,
- name: props.intl.formatMessage({
- id: 'kbn.dashboard.topNav.addPanel.savedSearchTabName',
- defaultMessage: 'Saved Search',
- }),
- dataTestSubj: 'addSavedSearchTab',
- toastDataTestSubj: 'addSavedSearchToDashboardSuccess',
- savedObjectFinder: (
-
- )
- }];
-
- this.state = {
- tabs: tabs,
- selectedTab: tabs[0],
- };
- }
-
- onSelectedTabChanged = tab => {
- this.setState({
- selectedTab: tab,
- });
- }
-
- renderTabs() {
- return this.state.tabs.map((tab) => {
- return (
- this.onSelectedTabChanged(tab)}
- isSelected={tab.id === this.state.selectedTab.id}
- key={tab.id}
- data-test-subj={tab.dataTestSubj}
- >
- {tab.name}
-
- );
- });
- }
-
- onAddPanel = (id, type) => {
+export class DashboardAddPanel extends React.Component {
+ onAddPanel = (id, type, name) => {
this.props.addNewPanel(id, type);
// To avoid the clutter of having toast messages cover flyout
@@ -131,53 +46,66 @@ class DashboardAddPanelUi extends React.Component {
}
this.lastToast = toastNotifications.addSuccess({
- title: this.props.intl.formatMessage({
- id: 'kbn.dashboard.topNav.addPanel.selectedTabAddedToDashboardSuccessMessageTitle',
- defaultMessage: '{selectedTabName} was added to your dashboard',
- }, {
- selectedTabName: this.state.selectedTab.name,
- }),
- 'data-test-subj': this.state.selectedTab.toastDataTestSubj,
+ title: i18n.translate(
+ 'kbn.dashboard.topNav.addPanel.savedObjectAddedToDashboardSuccessMessageTitle',
+ {
+ defaultMessage: '{savedObjectName} was added to your dashboard',
+ values: {
+ savedObjectName: name,
+ },
+ }
+ ),
+ 'data-test-subj': 'addObjectToDashboardSuccess',
});
- }
+ };
render() {
return (
-
-
-
-
-
+
+
+
+
-
+
-
-
- {this.renderTabs()}
-
-
-
-
- {this.state.selectedTab.savedObjectFinder}
-
+
+
+ Boolean(embeddableFactory.savedObjectMetaData))
+ .map(({ savedObjectMetaData }) => savedObjectMetaData)}
+ showFilter={true}
+ noItemsMessage={i18n.translate(
+ 'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage',
+ {
+ defaultMessage: 'No matching objects found.',
+ }
+ )}
+ />
+
+
+
+
+
+
+
+
+
);
}
}
-DashboardAddPanelUi.propTypes = {
+DashboardAddPanel.propTypes = {
onClose: PropTypes.func.isRequired,
- visTypes: PropTypes.object.isRequired,
addNewPanel: PropTypes.func.isRequired,
addNewVis: PropTypes.func.isRequired,
};
-
-export const DashboardAddPanel = injectI18n(DashboardAddPanelUi);
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js
index 3f233eed6b100..eccf9198939e3 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js
@@ -19,7 +19,7 @@
import React from 'react';
import sinon from 'sinon';
-import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { shallow } from 'enzyme';
import {
DashboardAddPanel,
@@ -38,11 +38,12 @@ beforeEach(() => {
});
test('render', () => {
- const component = shallowWithIntl( {}}
addNewVis={() => {}}
+ embeddableFactories={[]}
/>);
expect(component).toMatchSnapshot();
});
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js
index adb0908a623eb..ede1432d10480 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js
@@ -24,7 +24,7 @@ import ReactDOM from 'react-dom';
let isOpen = false;
-export function showAddPanel(addNewPanel, addNewVis, visTypes) {
+export function showAddPanel(addNewPanel, addNewVis, embeddableFactories) {
if (isOpen) {
return;
}
@@ -47,9 +47,9 @@ export function showAddPanel(addNewPanel, addNewVis, visTypes) {
);
diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts
index 83028ea62d881..17a5afb749f2e 100644
--- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts
@@ -19,6 +19,7 @@
import 'ui/doc_table';
+import { i18n } from '@kbn/i18n';
import { EmbeddableFactory } from 'ui/embeddable';
import {
EmbeddableInstanceConfiguration,
@@ -33,7 +34,16 @@ export class SearchEmbeddableFactory extends EmbeddableFactory {
private $rootScope: ng.IRootScopeService,
private searchLoader: SavedSearchLoader
) {
- super({ name: 'search' });
+ super({
+ name: 'search',
+ savedObjectMetaData: {
+ name: i18n.translate('kbn.discover.savedSearch.savedObjectName', {
+ defaultMessage: 'Saved search',
+ }),
+ type: 'search',
+ getIconForSavedObject: () => 'search',
+ },
+ });
}
public getEditPath(panelId: string) {
diff --git a/src/legacy/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/legacy/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap
index 05b8fae9ab1dd..6c4eaa7f2f4c7 100644
--- a/src/legacy/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap
@@ -10,27 +10,60 @@ exports[`render 1`] = `
ownFocus={true}
size="m"
>
-
+
-
+
-
+
-
+
+
+ }
+ onChoose={[Function]}
+ savedObjectMetaData={
+ Array [
+ Object {
+ "getIconForSavedObject": [Function],
+ "name": "Saved search",
+ "type": "search",
+ },
+ ]
+ }
+ />
+
+
+
+
- }
- makeUrl={[Function]}
- noItemsMessage={
-
- }
- onChoose={[Function]}
- savedObjectType="search"
- />
-
+
+
+
`;
diff --git a/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js b/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js
index 1235272588fcb..aee33d9dcb604 100644
--- a/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js
+++ b/src/legacy/core_plugins/kibana/public/discover/top_nav/open_search_panel.js
@@ -21,71 +21,76 @@ 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiSpacer,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutFooter,
EuiFlyoutBody,
EuiTitle,
- EuiButton,
} from '@elastic/eui';
const SEARCH_OBJECT_TYPE = 'search';
-export class OpenSearchPanel extends React.Component {
-
- renderMangageSearchesButton() {
- return (
-
-
+
+
+
+
+
+
+
+
+
+ }
+ savedObjectMetaData={[
+ {
+ type: SEARCH_OBJECT_TYPE,
+ getIconForSavedObject: () => 'search',
+ name: i18n.translate('kbn.discover.savedSearch.savedObjectName', {
+ defaultMessage: 'Saved search',
+ }),
+ },
+ ]}
+ onChoose={id => {
+ window.location.assign(props.makeUrl(id));
+ props.onClose();
+ }}
/>
-
- );
- }
-
- render() {
- return (
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
- }
- savedObjectType={SEARCH_OBJECT_TYPE}
- makeUrl={this.props.makeUrl}
- onChoose={this.props.onClose}
- callToActionButton={this.renderMangageSearchesButton()}
- />
-
-
-
- );
- }
+
+
+
+
+
+ );
}
OpenSearchPanel.propTypes = {
diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts
index e7a81a61b9831..ef1debdb218e9 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { i18n } from '@kbn/i18n';
+import chrome from 'ui/chrome';
import { EmbeddableFactory } from 'ui/embeddable';
import { getVisualizeLoader } from 'ui/visualize/loader';
import { VisualizeEmbeddable } from './visualize_embeddable';
@@ -26,16 +28,45 @@ import {
EmbeddableInstanceConfiguration,
OnEmbeddableStateChanged,
} from 'ui/embeddable/embeddable_factory';
+import { VisTypesRegistry } from 'ui/registry/vis_types';
+import { VisualizationAttributes } from '../../../../../server/saved_objects/service/saved_objects_client';
import { SavedVisualizations } from '../types';
import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
import { getIndexPattern } from './get_index_pattern';
-export class VisualizeEmbeddableFactory extends EmbeddableFactory {
+export class VisualizeEmbeddableFactory extends EmbeddableFactory {
private savedVisualizations: SavedVisualizations;
private config: Legacy.KibanaConfig;
- constructor(savedVisualizations: SavedVisualizations, config: Legacy.KibanaConfig) {
- super({ name: 'visualization' });
+ constructor(
+ savedVisualizations: SavedVisualizations,
+ config: Legacy.KibanaConfig,
+ visTypes: VisTypesRegistry
+ ) {
+ super({
+ name: 'visualization',
+ savedObjectMetaData: {
+ name: i18n.translate('kbn.visualize.savedObjectName', { defaultMessage: 'Visualization' }),
+ type: 'visualization',
+ getIconForSavedObject: savedObject => {
+ return (
+ visTypes.byName[JSON.parse(savedObject.attributes.visState).type].icon || 'visualizeApp'
+ );
+ },
+ getTooltipForSavedObject: savedObject => {
+ const visType = visTypes.byName[JSON.parse(savedObject.attributes.visState).type].title;
+ return `${savedObject.attributes.title} (${visType})`;
+ },
+ showSavedObject: savedObject => {
+ if (chrome.getUiSettingsClient().get('visualize:enableLabs')) {
+ return true;
+ }
+ const typeName: string = JSON.parse(savedObject.attributes.visState).type;
+ const visType = visTypes.byName[typeName];
+ return visType.stage !== 'experimental';
+ },
+ },
+ });
this.config = config;
this.savedVisualizations = savedVisualizations;
}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts
index da30433d11d72..dcdf58a52d918 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts
@@ -20,6 +20,7 @@
import { Legacy } from 'kibana';
import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry';
import { IPrivate } from 'ui/private';
+import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { SavedVisualizations } from '../types';
import { VisualizeEmbeddableFactory } from './visualize_embeddable_factory';
@@ -28,7 +29,11 @@ export function visualizeEmbeddableFactoryProvider(Private: IPrivate) {
savedVisualizations: SavedVisualizations,
config: Legacy.KibanaConfig
) => {
- return new VisualizeEmbeddableFactory(savedVisualizations, config);
+ return new VisualizeEmbeddableFactory(
+ savedVisualizations,
+ config,
+ Private(VisTypesRegistryProvider)
+ );
};
return Private(VisualizeEmbeddableFactoryProvider);
}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/_dialog.scss b/src/legacy/core_plugins/kibana/public/visualize/wizard/_dialog.scss
index 01ecbd9ff5197..829c18c3644f0 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/wizard/_dialog.scss
+++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/_dialog.scss
@@ -7,7 +7,8 @@
}
.visNewVisSearchDialog {
- min-height: $euiSizeL * 20;
+ width: $euiSizeL * 30;
+ min-height: $euiSizeL * 25;
}
.visNewVisDialog__body {
diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/search_selection/search_selection.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/search_selection/search_selection.tsx
index 2c945a5ccccbf..34c95b43991e8 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/wizard/search_selection/search_selection.tsx
+++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/search_selection/search_selection.tsx
@@ -17,14 +17,7 @@
* under the License.
*/
-import {
- EuiModalBody,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiSpacer,
- EuiTab,
- EuiTabs,
-} from '@elastic/eui';
+import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
@@ -38,22 +31,7 @@ interface SearchSelectionProps {
visType: VisType;
}
-interface SearchSelectionState {
- selectedTabId: string;
-}
-
-interface TabProps {
- id: string;
- name: string;
-}
-
-const INDEX_PATTERNS_TAB_ID = 'indexPatterns';
-const SAVED_SEARCHES_TAB_ID = 'savedSearches';
-
-export class SearchSelection extends React.Component {
- public state = {
- selectedTabId: INDEX_PATTERNS_TAB_ID,
- };
+export class SearchSelection extends React.Component {
private fixedPageSize: number = 8;
public render() {
@@ -74,77 +52,42 @@ export class SearchSelection extends React.Component
- {this.renderTabs()}
-
-
-
- {this.renderTab()}
+ 'search',
+ name: i18n.translate(
+ 'kbn.visualize.newVisWizard.searchSelection.savedObjectType.search',
+ {
+ defaultMessage: 'Saved search',
+ }
+ ),
+ },
+ {
+ type: 'index-pattern',
+ getIconForSavedObject: () => 'indexPatternApp',
+ name: i18n.translate(
+ 'kbn.visualize.newVisWizard.searchSelection.savedObjectType.indexPattern',
+ {
+ defaultMessage: 'Index pattern',
+ }
+ ),
+ },
+ ]}
+ fixedPageSize={this.fixedPageSize}
+ />
);
}
-
- private onSelectedTabChanged = (tab: TabProps) => {
- this.setState({
- selectedTabId: tab.id,
- });
- };
-
- private renderTabs() {
- const tabs = [
- {
- id: INDEX_PATTERNS_TAB_ID,
- name: i18n.translate('kbn.visualize.newVisWizard.indexPatternTabLabel', {
- defaultMessage: 'Index pattern',
- }),
- },
- {
- id: SAVED_SEARCHES_TAB_ID,
- name: i18n.translate('kbn.visualize.newVisWizard.savedSearchTabLabel', {
- defaultMessage: 'Saved search',
- }),
- },
- ];
- const { selectedTabId } = this.state;
-
- return tabs.map(tab => (
- this.onSelectedTabChanged(tab)}
- isSelected={tab.id === selectedTabId}
- key={tab.id}
- data-test-subj={`${tab.id}Tab`}
- >
- {tab.name}
-
- ));
- }
-
- private renderTab() {
- if (this.state.selectedTabId === SAVED_SEARCHES_TAB_ID) {
- return (
-
- );
- }
-
- return (
-
- );
- }
}
diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.d.ts b/src/legacy/server/saved_objects/service/saved_objects_client.d.ts
index 2b9db99dc7275..41340f8448576 100644
--- a/src/legacy/server/saved_objects/service/saved_objects_client.d.ts
+++ b/src/legacy/server/saved_objects/service/saved_objects_client.d.ts
@@ -83,6 +83,10 @@ export interface SavedObjectAttributes {
[key: string]: SavedObjectAttributes | string | number | boolean | null;
}
+export interface VisualizationAttributes extends SavedObjectAttributes {
+ visState: string;
+}
+
export interface SavedObject {
id: string;
type: string;
diff --git a/src/legacy/ui/public/embeddable/embeddable_factory.ts b/src/legacy/ui/public/embeddable/embeddable_factory.ts
index 07bfd2a2b33f0..e71813b22995f 100644
--- a/src/legacy/ui/public/embeddable/embeddable_factory.ts
+++ b/src/legacy/ui/public/embeddable/embeddable_factory.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { SavedObjectAttributes } from '../../../server/saved_objects';
+import { SavedObjectMetaData } from '../saved_objects/components/saved_object_finder';
import { Embeddable } from './embeddable';
import { EmbeddableState } from './types';
export interface EmbeddableInstanceConfiguration {
@@ -28,16 +30,24 @@ export type OnEmbeddableStateChanged = (embeddableStateChanges: EmbeddableState)
/**
* The EmbeddableFactory creates and initializes an embeddable instance
*/
-export abstract class EmbeddableFactory {
+export abstract class EmbeddableFactory {
public readonly name: string;
+ public readonly savedObjectMetaData?: SavedObjectMetaData;
/**
*
* @param name - a unique identified for this factory, which will be used to map an embeddable spec to
* a factory that can generate an instance of it.
*/
- constructor({ name }: { name: string }) {
+ constructor({
+ name,
+ savedObjectMetaData,
+ }: {
+ name: string;
+ savedObjectMetaData?: SavedObjectMetaData;
+ }) {
this.name = name;
+ this.savedObjectMetaData = savedObjectMetaData;
}
/**
diff --git a/src/legacy/ui/public/registry/_registry.d.ts b/src/legacy/ui/public/registry/_registry.d.ts
index 425ab45036519..9b95a2e02ee90 100644
--- a/src/legacy/ui/public/registry/_registry.d.ts
+++ b/src/legacy/ui/public/registry/_registry.d.ts
@@ -19,13 +19,21 @@
import { IndexedArray, IndexedArrayConfig } from '../indexed_array';
-interface UIRegistry extends IndexedArray {
- register(privateModule: T): UIRegistry;
-}
+interface UIRegistry extends IndexedArray {}
interface UIRegistrySpec extends IndexedArrayConfig {
name: string;
filter?(item: T): boolean;
}
-declare function uiRegistry(spec: UIRegistrySpec): UIRegistry;
+/**
+ * Creates a new UiRegistry (See js method for detailed documentation)
+ * The generic type T is the type of objects which are stored in the registry.
+ * The generic type A is an interface of accessors which depend on the
+ * fields of the objects stored in the registry.
+ * Example: if there is a string field "name" in type T, then A should be
+ * `{ byName: { [typeName: string]: T }; }`
+ */
+declare function uiRegistry(
+ spec: UIRegistrySpec
+): { (): UIRegistry & A; register(privateModule: T): UIRegistry & A };
diff --git a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts b/src/legacy/ui/public/registry/chrome_header_nav_controls.ts
index 5207113db6899..a626d013dde7b 100644
--- a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts
+++ b/src/legacy/ui/public/registry/chrome_header_nav_controls.ts
@@ -21,17 +21,18 @@ import { NavControl } from '../chrome/directives/header_global_nav';
import { IndexedArray } from '../indexed_array';
import { uiRegistry, UIRegistry } from './_registry';
-interface BySideDictionary {
- // this key should be from NavControlSide
- [side: string]: IndexedArray;
+interface ChromeHeaderNavControlsRegistryAccessors {
+ bySide: { [typeName: string]: IndexedArray };
}
-export interface ChromeHeaderNavControlsRegistry extends UIRegistry {
- bySide: BySideDictionary;
-}
+export type ChromeHeaderNavControlsRegistry = UIRegistry &
+ ChromeHeaderNavControlsRegistryAccessors;
-export const chromeHeaderNavControlsRegistry: ChromeHeaderNavControlsRegistry = uiRegistry({
+export const chromeHeaderNavControlsRegistry = uiRegistry<
+ NavControl,
+ ChromeHeaderNavControlsRegistryAccessors
+>({
name: 'chromeHeaderNavControls',
order: ['order'],
group: ['side'],
-}) as ChromeHeaderNavControlsRegistry;
+});
diff --git a/src/legacy/ui/public/registry/vis_types.js b/src/legacy/ui/public/registry/vis_types.js
deleted file mode 100644
index a6e95f5576d57..0000000000000
--- a/src/legacy/ui/public/registry/vis_types.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { uiRegistry } from './_registry';
-
-export const VisTypesRegistryProvider = uiRegistry({
- name: 'visTypes',
- index: ['name'],
- order: ['title']
-});
diff --git a/src/legacy/ui/public/registry/vis_types.d.ts b/src/legacy/ui/public/registry/vis_types.ts
similarity index 72%
rename from src/legacy/ui/public/registry/vis_types.d.ts
rename to src/legacy/ui/public/registry/vis_types.ts
index 77d5515dc5745..7f4c2e96eee44 100644
--- a/src/legacy/ui/public/registry/vis_types.d.ts
+++ b/src/legacy/ui/public/registry/vis_types.ts
@@ -18,8 +18,16 @@
*/
import { VisType } from '../vis';
-import { UIRegistry } from './_registry';
+import { uiRegistry, UIRegistry } from './_registry';
-declare type VisTypesRegistryProvider = UIRegistry & {
+interface VisTypesRegistryAccessors {
byName: { [typeName: string]: VisType };
-};
+}
+
+export type VisTypesRegistry = UIRegistry & VisTypesRegistryAccessors;
+
+export const VisTypesRegistryProvider = uiRegistry({
+ name: 'visTypes',
+ index: ['name'],
+ order: ['title'],
+});
diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.test.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.test.tsx
new file mode 100644
index 0000000000000..09412322a5558
--- /dev/null
+++ b/src/legacy/ui/public/saved_objects/components/saved_object_finder.test.tsx
@@ -0,0 +1,468 @@
+/*
+ * 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.
+ */
+
+jest.mock('ui/chrome', () => ({
+ getUiSettingsClient: () => ({
+ get: () => 10,
+ }),
+}));
+
+jest.mock('lodash', () => ({
+ debounce: (fn: any) => fn,
+}));
+
+const nextTick = () => new Promise(res => process.nextTick(res));
+
+import {
+ EuiEmptyPrompt,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiLoadingSpinner,
+ EuiPagination,
+ EuiTablePagination,
+} from '@elastic/eui';
+import { shallow } from 'enzyme';
+import React from 'react';
+import * as sinon from 'sinon';
+import { SavedObjectFinder } from './saved_object_finder';
+
+describe('SavedObjectsFinder', () => {
+ let objectsClientStub: sinon.SinonStub;
+
+ const doc = {
+ id: '1',
+ type: 'search',
+ attributes: { title: 'Example title' },
+ };
+
+ const doc2 = {
+ id: '2',
+ type: 'search',
+ attributes: { title: 'Another title' },
+ };
+
+ const doc3 = { type: 'vis', id: '3', attributes: { title: 'Vis' } };
+
+ const searchMetaData = [
+ {
+ type: 'search',
+ name: 'Search',
+ getIconForSavedObject: () => 'search',
+ showSavedObject: () => true,
+ },
+ ];
+
+ beforeEach(() => {
+ objectsClientStub = sinon.stub();
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [] }));
+ require('ui/chrome').getSavedObjectsClient = () => ({
+ find: async (...args: any[]) => {
+ return objectsClientStub(...args);
+ },
+ });
+ });
+
+ it('should call saved object client on startup', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ expect(
+ objectsClientStub.calledWith({
+ type: ['search'],
+ fields: ['title', 'visState'],
+ search: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ })
+ ).toBe(true);
+ });
+
+ it('should list initial items', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(
+ wrapper.containsMatchingElement()
+ ).toEqual(true);
+ });
+
+ it('should call onChoose on item click', async () => {
+ const chooseStub = sinon.stub();
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] }));
+
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find(EuiListGroupItem)
+ .first()
+ .simulate('click');
+ expect(chooseStub.calledWith('1', 'search', `${doc.attributes.title} (Search)`)).toEqual(true);
+ });
+
+ describe('sorting', () => {
+ it('should list items ascending', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ const list = wrapper.find(EuiListGroup);
+ expect(list.childAt(0).key()).toBe('2');
+ expect(list.childAt(1).key()).toBe('1');
+ });
+
+ it('should list items descending', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.setState({ sortDirection: 'desc' });
+ const list = wrapper.find(EuiListGroup);
+ expect(list.childAt(0).key()).toBe('1');
+ expect(list.childAt(1).key()).toBe('2');
+ });
+ });
+
+ it('should not show the saved objects which get filtered by showSavedObject', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] }));
+
+ const wrapper = shallow(
+ 'search',
+ showSavedObject: ({ id }) => id !== '1',
+ },
+ ]}
+ />
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ const list = wrapper.find(EuiListGroup);
+ expect(list.childAt(0).key()).toBe('2');
+ expect(list.children().length).toBe(1);
+ });
+
+ describe('search', () => {
+ it('should request filtered list on search input', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"]')
+ .first()
+ .simulate('change', { target: { value: 'abc' } });
+
+ expect(
+ objectsClientStub.calledWith({
+ type: ['search'],
+ fields: ['title', 'visState'],
+ search: 'abc*',
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ })
+ ).toBe(true);
+ });
+
+ it('should respect response order on search input', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc, doc2] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"]')
+ .first()
+ .simulate('change', { target: { value: 'abc' } });
+ await nextTick();
+ const list = wrapper.find(EuiListGroup);
+ expect(list.childAt(0).key()).toBe('1');
+ expect(list.childAt(1).key()).toBe('2');
+ });
+ });
+
+ it('should request multiple saved object types at once', async () => {
+ const wrapper = shallow(
+ 'search',
+ },
+ {
+ type: 'vis',
+ name: 'Vis',
+ getIconForSavedObject: () => 'visualization',
+ },
+ ]}
+ />
+ );
+ wrapper.instance().componentDidMount!();
+
+ expect(
+ objectsClientStub.calledWith({
+ type: ['search', 'vis'],
+ fields: ['title', 'visState'],
+ search: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ })
+ ).toBe(true);
+ });
+
+ describe('filter', () => {
+ const metaDataConfig = [
+ {
+ type: 'search',
+ name: 'Search',
+ getIconForSavedObject: () => 'search',
+ },
+ {
+ type: 'vis',
+ name: 'Vis',
+ getIconForSavedObject: () => 'document',
+ },
+ ];
+
+ it('should not render filter buttons if disabled', async () => {
+ objectsClientStub.returns(
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.find('[data-test-subj="savedObjectFinderFilter-search"]').exists()).toBe(
+ false
+ );
+ });
+
+ it('should not render filter buttons if there is only one type in the list', async () => {
+ objectsClientStub.returns(
+ Promise.resolve({
+ savedObjects: [doc, doc2],
+ })
+ );
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.find('[data-test-subj="savedObjectFinderFilter-search"]').exists()).toBe(
+ false
+ );
+ });
+
+ it('should apply filter if selected', async () => {
+ objectsClientStub.returns(
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.setState({ filteredTypes: ['vis'] });
+ const list = wrapper.find(EuiListGroup);
+ expect(list.childAt(0).key()).toBe('3');
+ expect(list.children().length).toBe(1);
+
+ wrapper.setState({ filteredTypes: ['vis', 'search'] });
+ expect(wrapper.find(EuiListGroup).children().length).toBe(3);
+ });
+ });
+
+ it('should display no items message if there are no items', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [] }));
+ const noItemsMessage = ;
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+
+ expect(
+ wrapper
+ .find(EuiEmptyPrompt)
+ .first()
+ .prop('body')
+ ).toEqual(noItemsMessage);
+ });
+
+ describe('pagination', () => {
+ const longItemList = new Array(50).fill(undefined).map((_, i) => ({
+ id: String(i),
+ type: 'search',
+ attributes: {
+ title: `Title ${i < 10 ? '0' : ''}${i}`,
+ },
+ }));
+
+ beforeEach(() => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: longItemList }));
+ });
+
+ it('should show a table pagination with initial per page', async () => {
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(
+ wrapper
+ .find(EuiTablePagination)
+ .first()
+ .prop('itemsPerPage')
+ ).toEqual(15);
+ expect(wrapper.find(EuiListGroup).children().length).toBe(15);
+ });
+
+ it('should allow switching the page size', async () => {
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find(EuiTablePagination)
+ .first()
+ .prop('onChangeItemsPerPage')!(5);
+ expect(wrapper.find(EuiListGroup).children().length).toBe(5);
+ });
+
+ it('should switch page correctly', async () => {
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find(EuiTablePagination)
+ .first()
+ .prop('onChangePage')!(1);
+ expect(
+ wrapper
+ .find(EuiListGroup)
+ .children()
+ .first()
+ .key()
+ ).toBe('15');
+ });
+
+ it('should show an ordinary pagination for fixed page sizes', async () => {
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(
+ wrapper
+ .find(EuiPagination)
+ .first()
+ .prop('pageCount')
+ ).toEqual(2);
+ expect(wrapper.find(EuiListGroup).children().length).toBe(33);
+ });
+
+ it('should switch page correctly for fixed page sizes', async () => {
+ const wrapper = shallow(
+
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find(EuiPagination)
+ .first()
+ .prop('onPageClick')!(1);
+ expect(
+ wrapper
+ .find(EuiListGroup)
+ .children()
+ .first()
+ .key()
+ ).toBe('33');
+ });
+ });
+
+ describe('loading state', () => {
+ it('should display a spinner during initial loading', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.containsMatchingElement()).toBe(true);
+ });
+
+ it('should hide the spinner if data is shown', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] }));
+
+ const wrapper = shallow(
+ 'search',
+ },
+ ]}
+ />
+ );
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.containsMatchingElement()).toBe(false);
+ });
+
+ it('should not show the spinner if there are already items', async () => {
+ objectsClientStub.returns(Promise.resolve({ savedObjects: [doc] }));
+
+ const wrapper = shallow();
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"]')
+ .first()
+ .simulate('change', { target: { value: 'abc' } });
+
+ wrapper.update();
+
+ expect(wrapper.containsMatchingElement()).toBe(false);
+ });
+ });
+});
diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx
index 481eca0edaf67..8575fa66ee747 100644
--- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx
+++ b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx
@@ -23,44 +23,74 @@ import React from 'react';
import chrome from 'ui/chrome';
import {
- EuiBasicTable,
+ CommonProps,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiContextMenuPanelProps,
+ EuiEmptyPrompt,
EuiFieldSearch,
+ EuiFilterButton,
+ EuiFilterGroup,
EuiFlexGroup,
EuiFlexItem,
- EuiLink,
- EuiTableCriteria,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiLoadingSpinner,
+ EuiPagination,
+ EuiPopover,
+ EuiSpacer,
+ EuiTablePagination,
} from '@elastic/eui';
import { Direction } from '@elastic/eui/src/services/sort/sort_direction';
import { i18n } from '@kbn/i18n';
import { SavedObjectAttributes } from '../../../../server/saved_objects';
-import { VisTypesRegistryProvider } from '../../registry/vis_types';
import { SimpleSavedObject } from '../simple_saved_object';
-interface SavedObjectFinderUIState {
+// TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted
+const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent<
+ CommonProps & { maxWidth: boolean }
+>;
+
+// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted
+const FixedEuiContextMenuPanel = (EuiContextMenuPanel as any) as React.FunctionComponent<
+ EuiContextMenuPanelProps & { watchedItemProps: string[] }
+>;
+
+export interface SavedObjectMetaData {
+ type: string;
+ name: string;
+ getIconForSavedObject(savedObject: SimpleSavedObject): string | undefined;
+ getTooltipForSavedObject?(savedObject: SimpleSavedObject): string;
+ showSavedObject?(savedObject: SimpleSavedObject): boolean;
+}
+
+interface SavedObjectFinderState {
items: Array<{
title: string | null;
id: SimpleSavedObject['id'];
type: SimpleSavedObject['type'];
+ savedObject: SimpleSavedObject;
}>;
- filter: string;
+ query: string;
isFetchingItems: boolean;
page: number;
perPage: number;
- sortField?: string;
sortDirection?: Direction;
+ sortOpen: boolean;
+ filterOpen: boolean;
+ filteredTypes: string[];
}
interface BaseSavedObjectFinder {
- callToActionButton?: React.ReactNode;
onChoose?: (
id: SimpleSavedObject['id'],
- type: SimpleSavedObject['type']
+ type: SimpleSavedObject['type'],
+ name: string
) => void;
- makeUrl?: (id: SimpleSavedObject['id']) => void;
noItemsMessage?: React.ReactNode;
- savedObjectType: 'visualization' | 'search' | 'index-pattern';
- visTypes?: VisTypesRegistryProvider;
+ savedObjectMetaData: Array>;
+ showFilter?: boolean;
}
interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder {
@@ -69,51 +99,44 @@ interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder {
}
interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder {
- initialPageSize?: 5 | 10 | 15;
+ initialPageSize?: 5 | 10 | 15 | 25;
fixedPageSize?: undefined;
}
type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize;
-class SavedObjectFinder extends React.Component {
+class SavedObjectFinder extends React.Component {
public static propTypes = {
- callToActionButton: PropTypes.node,
onChoose: PropTypes.func,
- makeUrl: PropTypes.func,
noItemsMessage: PropTypes.node,
- savedObjectType: PropTypes.oneOf(['visualization', 'search', 'index-pattern']).isRequired,
- visTypes: PropTypes.object,
- initialPageSize: PropTypes.oneOf([5, 10, 15]),
+ savedObjectMetaData: PropTypes.array.isRequired,
+ initialPageSize: PropTypes.oneOf([5, 10, 15, 25]),
fixedPageSize: PropTypes.number,
+ showFilter: PropTypes.bool,
};
private isComponentMounted: boolean = false;
- private debouncedFetch = _.debounce(async (filter: string) => {
+ private debouncedFetch = _.debounce(async (query: string) => {
+ const metaDataMap = this.getSavedObjectMetaDataMap();
+
const resp = await chrome.getSavedObjectsClient().find({
- type: this.props.savedObjectType,
+ type: Object.keys(metaDataMap),
fields: ['title', 'visState'],
- search: filter ? `${filter}*` : undefined,
+ search: query ? `${query}*` : undefined,
page: 1,
perPage: chrome.getUiSettingsClient().get('savedObjects:listingLimit'),
searchFields: ['title^3', 'description'],
defaultSearchOperator: 'AND',
});
- const { savedObjectType, visTypes } = this.props;
- if (
- savedObjectType === 'visualization' &&
- !chrome.getUiSettingsClient().get('visualize:enableLabs') &&
- visTypes
- ) {
- resp.savedObjects = resp.savedObjects.filter(savedObject => {
- if (typeof savedObject.attributes.visState !== 'string') {
- return false;
- }
- const typeName: string = JSON.parse(savedObject.attributes.visState).type;
- const visType = visTypes.byName[typeName];
- return visType.stage !== 'experimental';
- });
- }
+ resp.savedObjects = resp.savedObjects.filter(savedObject => {
+ const metaData = metaDataMap[savedObject.type];
+ if (metaData.showSavedObject) {
+ return metaData.showSavedObject(savedObject);
+ } else {
+ return true;
+ }
+ });
if (!this.isComponentMounted) {
return;
@@ -121,14 +144,20 @@ class SavedObjectFinder extends React.Component {
+ items: resp.savedObjects.map(savedObject => {
+ const {
+ attributes: { title },
+ id,
+ type,
+ } = savedObject;
return {
title: typeof title === 'string' ? title : '',
id,
type,
+ savedObject,
};
}),
});
@@ -142,8 +171,11 @@ class SavedObjectFinder extends React.Component
{this.renderSearchBar()}
- {this.renderTable()}
+ {this.renderListing()}
);
}
- private onTableChange = ({ page, sort = {} }: EuiTableCriteria) => {
- let sortField: string | undefined = sort.field;
- let sortDirection: Direction | undefined = sort.direction;
-
- // 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 = undefined;
- sortDirection = undefined;
- }
+ private getSavedObjectMetaDataMap(): Record> {
+ return this.props.savedObjectMetaData.reduce(
+ (map, metaData) => ({ ...map, [metaData.type]: metaData }),
+ {}
+ );
+ }
- this.setState({
- page: page.index,
- perPage: page.size,
- sortField,
- sortDirection,
- });
- };
+ private getPageCount() {
+ return Math.ceil(
+ (this.state.filteredTypes.length === 0
+ ? this.state.items.length
+ : this.state.items.filter(
+ item =>
+ this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type)
+ ).length) / this.state.perPage
+ );
+ }
// server-side paging not supported
// 1) saved object client does not support sorting by title because title is only mapped as analyzed
@@ -197,17 +224,15 @@ class SavedObjectFinder extends React.Component {
// do not sort original list to preserve elasticsearch ranking order
const items = this.state.items.slice();
- const { sortField } = this.state;
+ const { sortDirection } = this.state;
- if (sortField) {
- items.sort((a, b) => {
- const fieldA = _.get(a, sortField, '');
- const fieldB = _.get(b, sortField, '');
+ if (sortDirection || !this.state.query) {
+ items.sort(({ title: titleA }, { title: titleB }) => {
let order = 1;
- if (this.state.sortDirection === 'desc') {
+ if (sortDirection === 'desc') {
order = -1;
}
- return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase());
+ return order * (titleA || '').toLowerCase().localeCompare((titleB || '').toLowerCase());
});
}
@@ -215,7 +240,12 @@ class SavedObjectFinder extends React.Component
+ this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type)
+ )
+ .slice(startIndex, lastIndex);
};
private fetchItems = () => {
@@ -223,98 +253,262 @@ class SavedObjectFinder extends React.Component();
+ this.state.items.forEach(item => {
+ typesInItems.add(item.type);
+ });
+ return this.props.savedObjectMetaData.filter(metaData => typesInItems.has(metaData.type));
+ }
+
+ private getSortOptions() {
+ const sortOptions = [
+ {
+ this.setState({
+ sortDirection: 'asc',
+ });
+ }}
+ >
+ {i18n.translate('common.ui.savedObjects.finder.sortAsc', {
+ defaultMessage: 'Ascending',
+ })}
+ ,
+ {
+ this.setState({
+ sortDirection: 'desc',
+ });
+ }}
+ >
+ {i18n.translate('common.ui.savedObjects.finder.sortDesc', {
+ defaultMessage: 'Descending',
+ })}
+ ,
+ ];
+ if (this.state.query) {
+ sortOptions.push(
+ {
+ this.setState({
+ sortDirection: undefined,
+ });
+ }}
+ >
+ {i18n.translate('common.ui.savedObjects.finder.sortAuto', {
+ defaultMessage: 'Best match',
+ })}
+
+ );
+ }
+ return sortOptions;
+ }
+
private renderSearchBar() {
+ const availableSavedObjectMetaData = this.getAvailableSavedObjectMetaData();
+
return (
-
+
{
this.setState(
{
- filter: e.target.value,
+ query: e.target.value,
},
this.fetchItems
);
}}
data-test-subj="savedObjectFinderSearchInput"
+ isLoading={this.state.isFetchingItems}
/>
-
- {this.props.callToActionButton && (
- {this.props.callToActionButton}
- )}
+
+
+ this.setState({ sortOpen: false })}
+ button={
+
+ this.setState(({ sortOpen }) => ({
+ sortOpen: !sortOpen,
+ }))
+ }
+ iconType="arrowDown"
+ isSelected={this.state.sortOpen}
+ data-test-subj="savedObjectFinderSortButton"
+ >
+ {i18n.translate('common.ui.savedObjects.finder.sortButtonLabel', {
+ defaultMessage: 'Sort',
+ })}
+
+ }
+ >
+
+
+ {this.props.showFilter && (
+ this.setState({ filterOpen: false })}
+ button={
+
+ this.setState(({ filterOpen }) => ({
+ filterOpen: !filterOpen,
+ }))
+ }
+ iconType="arrowDown"
+ data-test-subj="savedObjectFinderFilterButton"
+ isSelected={this.state.filterOpen}
+ numFilters={this.props.savedObjectMetaData.length}
+ hasActiveFilters={this.state.filteredTypes.length > 0}
+ numActiveFilters={this.state.filteredTypes.length}
+ >
+ {i18n.translate('common.ui.savedObjects.finder.filterButtonLabel', {
+ defaultMessage: 'Types',
+ })}
+
+ }
+ >
+ (
+ {
+ this.setState(({ filteredTypes }) => ({
+ filteredTypes: filteredTypes.includes(metaData.type)
+ ? filteredTypes.filter(t => t !== metaData.type)
+ : [...filteredTypes, metaData.type],
+ page: 0,
+ }));
+ }}
+ >
+ {metaData.name}
+
+ ))}
+ />
+
+ )}
+
+
);
}
- private renderTable() {
- const pagination = {
- pageIndex: this.state.page,
- pageSize: this.state.perPage,
- totalItemCount: this.state.items.length,
- hidePerPageOptions: Boolean(this.props.fixedPageSize),
- pageSizeOptions: [5, 10, 15],
- };
- // TODO there should be a Type in EUI for that, replace if it exists
- const sorting: { sort?: EuiTableCriteria['sort'] } = {};
- if (this.state.sortField) {
- sorting.sort = {
- field: this.state.sortField,
- direction: this.state.sortDirection,
- };
- }
- const tableColumns = [
- {
- field: 'title',
- name: i18n.translate('common.ui.savedObjects.finder.titleLabel', {
- defaultMessage: 'Title',
- }),
- sortable: true,
- render: (title: string, record: SimpleSavedObject) => {
- const { onChoose, makeUrl } = this.props;
-
- if (!onChoose && !makeUrl) {
- return {title};
- }
-
- return (
- {
- onChoose(record.id, record.type);
- }
- : undefined
- }
- href={makeUrl ? makeUrl(record.id) : undefined}
- data-test-subj={`savedObjectTitle${title.split(' ').join('-')}`}
- >
- {title}
-
- );
- },
- },
- ];
+ private renderListing() {
const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
+ const { onChoose, savedObjectMetaData } = this.props;
+
return (
-
+ <>
+ {this.state.isFetchingItems && this.state.items.length === 0 && (
+
+
+
+
+
+
+ )}
+ {items.length > 0 ? (
+
+ {items.map(item => {
+ const currentSavedObjectMetaData = savedObjectMetaData.find(
+ metaData => metaData.type === item.type
+ )!;
+ const fullName = currentSavedObjectMetaData.getTooltipForSavedObject
+ ? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject)
+ : `${item.title} (${currentSavedObjectMetaData!.name})`;
+ const iconType = (
+ currentSavedObjectMetaData ||
+ ({
+ getIconForSavedObject: () => 'document',
+ } as Pick, 'getIconForSavedObject'>)
+ ).getIconForSavedObject(item.savedObject);
+ return (
+ {
+ onChoose(item.id, item.type, fullName);
+ }
+ : undefined
+ }
+ title={fullName}
+ data-test-subj={`savedObjectTitle${(item.title || '').split(' ').join('-')}`}
+ />
+ );
+ })}
+
+ ) : (
+ !this.state.isFetchingItems &&
+ )}
+ {this.getPageCount() > 1 &&
+ (this.props.fixedPageSize ? (
+ {
+ this.setState({
+ page,
+ });
+ }}
+ />
+ ) : (
+ {
+ this.setState({
+ page,
+ });
+ }}
+ onChangeItemsPerPage={perPage => {
+ this.setState({
+ perPage,
+ });
+ }}
+ itemsPerPage={this.state.perPage}
+ itemsPerPageOptions={[5, 10, 15, 25]}
+ />
+ ))}
+ >
);
}
}
diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js
index 8742851249b7b..eca064ef04646 100644
--- a/test/functional/apps/dashboard/dashboard_filtering.js
+++ b/test/functional/apps/dashboard/dashboard_filtering.js
@@ -46,9 +46,6 @@ export default function ({ getService, getPageObjects }) {
await dashboardAddPanel.addEveryVisualization('"Filter Bytes Test"');
await dashboardAddPanel.addEverySavedSearch('"Filter Bytes Test"');
- // TODO: Remove once https://github.com/elastic/kibana/issues/22561 is fixed
- await dashboardPanelActions.removePanelByTitle('Filter Bytes Test: timelion split 5 on bytes');
-
await dashboardAddPanel.closeAddPanel();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js
index 1c9ce3972c9c1..9962aa2049431 100644
--- a/test/functional/page_objects/common_page.js
+++ b/test/functional/page_objects/common_page.js
@@ -301,7 +301,13 @@ export function CommonPageProvider({ getService, getPageObjects }) {
}
async closeToast() {
- const toast = await find.byCssSelector('.euiToast');
+ let toast;
+ await retry.try(async () => {
+ toast = await find.byCssSelector('.euiToast');
+ if (!toast) {
+ throw new Error('Toast is not visible yet');
+ }
+ });
await browser.moveMouseTo(toast);
const title = await (await find.byCssSelector('.euiToastHeader__title')).getVisibleText();
log.debug(title);
diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js
index 3c2f36df21823..531fd9f984a0b 100644
--- a/test/functional/page_objects/discover_page.js
+++ b/test/functional/page_objects/discover_page.js
@@ -86,13 +86,13 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
}
async hasSavedSearch(searchName) {
- const searchLink = await find.byPartialLinkText(searchName);
+ const searchLink = await find.byButtonText(searchName);
return searchLink.isDisplayed();
}
async loadSavedSearch(searchName) {
await this.openLoadSavedSearchPanel();
- const searchLink = await find.byPartialLinkText(searchName);
+ const searchLink = await find.byButtonText(searchName);
await searchLink.click();
await PageObjects.header.waitUntilLoadingHasFinished();
}
diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js
index 7e7726a050405..c14e34687ddac 100644
--- a/test/functional/page_objects/visualize_page.js
+++ b/test/functional/page_objects/visualize_page.js
@@ -397,7 +397,6 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async clickSavedSearch(savedSearchName) {
- await testSubjects.click('savedSearchesTab');
await testSubjects.click(`savedObjectTitle${savedSearchName.split(' ').join('-')}`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
diff --git a/test/functional/services/dashboard/add_panel.js b/test/functional/services/dashboard/add_panel.js
index 18f5bea91d1a5..c8572125ac7c3 100644
--- a/test/functional/services/dashboard/add_panel.js
+++ b/test/functional/services/dashboard/add_panel.js
@@ -24,7 +24,6 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
const flyout = getService('flyout');
const PageObjects = getPageObjects(['header', 'common']);
- const find = getService('find');
return new class DashboardAddPanel {
async clickOpenAddPanel() {
@@ -36,14 +35,23 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
await testSubjects.click('addNewSavedObjectLink');
}
- async clickSavedSearchTab() {
- await testSubjects.click('addSavedSearchTab');
+ async toggleFilterPopover() {
+ log.debug('DashboardAddPanel.toggleFilter');
+ await testSubjects.click('savedObjectFinderFilterButton');
+ }
+
+ async toggleFilter(type) {
+ log.debug(`DashboardAddPanel.addToFilter(${type})`);
+ await this.waitForListLoading();
+ await this.toggleFilterPopover();
+ await testSubjects.click(`savedObjectFinderFilter-${type}`);
+ await this.toggleFilterPopover();
}
async addEveryEmbeddableOnCurrentPage() {
log.debug('addEveryEmbeddableOnCurrentPage');
- const addPanel = await testSubjects.find('dashboardAddPanel');
- const embeddableRows = await addPanel.findAllByClassName('euiLink');
+ const itemList = await testSubjects.find('savedObjectFinderItemList');
+ const embeddableRows = await itemList.findAllByCssSelector('li');
for (let i = 0; i < embeddableRows.length; i++) {
await embeddableRows[i].click();
await PageObjects.common.closeToast();
@@ -95,10 +103,9 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
}
}
- async waitForEuiTableLoading() {
+ async waitForListLoading() {
await retry.waitFor('dashboard add panel loading to complete', async () => {
- const table = await find.byClassName('euiBasicTable');
- return !((await table.getAttribute('class')).includes('loading'));
+ return !(await testSubjects.exists('savedObjectFinderLoadingIndicator'));
});
}
@@ -109,6 +116,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
async addEveryVisualization(filter) {
log.debug('DashboardAddPanel.addEveryVisualization');
await this.ensureAddPanelIsShowing();
+ await this.toggleFilter('visualization');
if (filter) {
await this.filterEmbeddableNames(filter.replace('-', ' '));
}
@@ -123,7 +131,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
async addEverySavedSearch(filter) {
log.debug('DashboardAddPanel.addEverySavedSearch');
await this.ensureAddPanelIsShowing();
- await this.clickSavedSearchTab();
+ await this.toggleFilter('search');
if (filter) {
await this.filterEmbeddableNames(filter.replace('-', ' '));
}
@@ -139,11 +147,11 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
log.debug(`addSavedSearch(${searchName})`);
await this.ensureAddPanelIsShowing();
- await this.clickSavedSearchTab();
+ await this.toggleFilter('search');
await this.filterEmbeddableNames(searchName);
await testSubjects.click(`savedObjectTitle${searchName.split(' ').join('-')}`);
- await testSubjects.exists('addSavedSearchToDashboardSuccess');
+ await testSubjects.exists('addObjectToDashboardSuccess');
await this.closeAddPanel();
}
@@ -163,16 +171,18 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
async addVisualization(vizName) {
log.debug(`DashboardAddPanel.addVisualization(${vizName})`);
await this.ensureAddPanelIsShowing();
+ await this.toggleFilter('visualization');
await this.filterEmbeddableNames(`"${vizName.replace('-', ' ')}"`);
await testSubjects.click(`savedObjectTitle${vizName.split(' ').join('-')}`);
+ await testSubjects.exists('addObjectToDashboardSuccess');
await this.closeAddPanel();
}
async filterEmbeddableNames(name) {
// The search input field may be disabled while the table is loading so wait for it
- await this.waitForEuiTableLoading();
+ await this.waitForListLoading();
await testSubjects.setValue('savedObjectFinderSearchInput', name);
- await this.waitForEuiTableLoading();
+ await this.waitForListLoading();
}
async panelAddLinkExists(name) {
diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts
index fcdc481c17770..bdd6d4d72fbd2 100644
--- a/typings/@elastic/eui/index.d.ts
+++ b/typings/@elastic/eui/index.d.ts
@@ -25,6 +25,7 @@ declare module '@elastic/eui' {
export const EuiCopy: React.SFC;
export const EuiOutsideClickDetector: React.SFC;
export const EuiSideNav: React.SFC;
+ export const EuiListGroupItem: React.FunctionComponent;
export interface EuiTableCriteria {
page: { index: number; size: number };
diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx
index 93efcdce0295a..6385e74280a12 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx
+++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx
@@ -19,7 +19,6 @@ import { NavControlSide } from 'ui/chrome/directives/header_global_nav';
import { I18nContext } from 'ui/i18n';
// @ts-ignore
import { uiModules } from 'ui/modules';
-// @ts-ignore
import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
// @ts-ignore
import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls';
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 59551144200e4..004e5b8c5b41a 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -577,7 +577,6 @@
"common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel": "保存“{name}”",
"common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage": "具有标题 “{title}” 的 “{name}” 已存在。是否确定要保存?",
"common.ui.savedObjects.finder.searchPlaceholder": "搜索……",
- "common.ui.savedObjects.finder.titleLabel": "标题",
"common.ui.savedObjects.howToSaveAsNewDescription": "在 Kibana 的以前版本中,更改 {savedObjectName} 的名称将创建具有新名称的副本。使用“另存为新的 {savedObjectName}” 复选框可立即达到此目的。",
"common.ui.savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认",
"common.ui.savedObjects.saveAsNewLabel": "另存为新的 {savedObjectName}",
@@ -1215,12 +1214,6 @@
"kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。",
"kbn.dashboard.strings.dashboardEditTitle": "编辑 {title}",
"kbn.dashboard.strings.dashboardUnsavedEditTitle": "编辑 {title}(未保存)",
- "kbn.dashboard.topNav.addPanel.addNewVisualizationButtonLabel": "添加新的可视化",
- "kbn.dashboard.topNav.addPanel.savedSearchTabName": "已保存搜索",
- "kbn.dashboard.topNav.addPanel.searchSavedObjectFinder.noMatchingVisualizationsMessage": "未找到匹配的已保存搜索。",
- "kbn.dashboard.topNav.addPanel.selectedTabAddedToDashboardSuccessMessageTitle": "“{selectedTabName}” 已添加到您的仪表板",
- "kbn.dashboard.topNav.addPanel.visSavedObjectFinder.noMatchingVisualizationsMessage": "未找到任何匹配的可视化。",
- "kbn.dashboard.topNav.addPanel.visualizationTabName": "可视化",
"kbn.dashboard.topNav.addPanelsTitle": "添加面板",
"kbn.dashboard.topNav.cloneModal.cancelButtonLabel": "取消",
"kbn.dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "克隆面板",
@@ -8203,4 +8196,4 @@
"xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
-}
\ No newline at end of file
+}