diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index d0ff54290256e..015cb6ddaf285 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -13,7 +13,8 @@ "navigation", "uiActions", "savedObjects", - "dataViewFieldEditor" + "dataViewFieldEditor", + "dataViewEditor" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "dataViews"], diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 5601596a4d73b..b7af922a058cd 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -48,7 +48,7 @@ import { import { FieldStatisticsTable } from '../field_stats_table'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; -import { DataViewType } from '../../../../../../data_views/common'; +import { DataViewType, DataView } from '../../../../../../data_views/common'; /** * Local storage key for sidebar persistence state @@ -204,6 +204,14 @@ export function DiscoverLayout({ }, [isSidebarClosed, storage]); const contentCentered = resultState === 'uninitialized' || resultState === 'none'; + const onDataViewCreated = useCallback( + (dataView: DataView) => { + if (dataView.id) { + onChangeIndexPattern(dataView.id); + } + }, + [onChangeIndexPattern] + ); return ( @@ -245,6 +253,7 @@ export function DiscoverLayout({ useNewFieldsApi={useNewFieldsApi} onEditRuntimeField={onEditRuntimeField} viewMode={viewMode} + onDataViewCreated={onDataViewCreated} /> diff --git a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index 94aa55ff23853..059c247c38c2e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -133,6 +133,7 @@ exports[`Discover DataView Management renders correctly 1`] = ` } >
{ const indexPattern = stubLogstashIndexPattern; const editField = jest.fn(); + const createNewDataView = jest.fn(); const mountComponent = () => { return mountWithIntl( @@ -62,6 +63,7 @@ describe('Discover DataView Management', () => { editField={editField} selectedIndexPattern={indexPattern} useNewFieldsApi={true} + createNewDataView={createNewDataView} /> ); @@ -81,7 +83,7 @@ describe('Discover DataView Management', () => { button.simulate('click'); expect(component.find(EuiContextMenuPanel).length).toBe(1); - expect(component.find(EuiContextMenuItem).length).toBe(2); + expect(component.find(EuiContextMenuItem).length).toBe(3); }); test('click on an add button executes editField callback', () => { @@ -103,4 +105,14 @@ describe('Discover DataView Management', () => { manageButton.simulate('click'); expect(mockServices.core.application.navigateToApp).toHaveBeenCalled(); }); + + test('click on add dataView button executes createNewDataView callback', () => { + const component = mountComponent(); + const button = findTestSubject(component, 'discoverIndexPatternActions'); + button.simulate('click'); + + const manageButton = findTestSubject(component, 'dataview-create-new'); + manageButton.simulate('click'); + expect(createNewDataView).toHaveBeenCalled(); + }); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx index 0655357d55983..b62e6e15c55af 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx @@ -7,7 +7,13 @@ */ import React, { useState } from 'react'; -import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiHorizontalRule, + EuiPopover, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; import { DataView } from '../../../../../../data/common'; @@ -26,11 +32,16 @@ export interface DiscoverIndexPatternManagementProps { * @param fieldName */ editField: (fieldName?: string) => void; + + /** + * Callback to execute on create new data action + */ + createNewDataView: () => void; } export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManagementProps) { const { dataViewFieldEditor, core } = useDiscoverServices(); - const { useNewFieldsApi, selectedIndexPattern, editField } = props; + const { useNewFieldsApi, selectedIndexPattern, editField, createNewDataView } = props; const dataViewEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern(); const canEditDataViewField = !!dataViewEditPermission && useNewFieldsApi; const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false); @@ -45,7 +56,7 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage return ( { setIsAddIndexPatternFieldPopoverOpen(false); @@ -67,7 +78,8 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage } > {i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', { - defaultMessage: 'Add field to data view', + defaultMessage: 'Add field', })} , {i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', { - defaultMessage: 'Manage data view fields', + defaultMessage: 'Manage settings', + })} + , + , + { + setIsAddIndexPatternFieldPopoverOpen(false); + createNewDataView(); + }} + > + {i18n.translate('discover.fieldChooser.dataViews.createNewDataView', { + defaultMessage: 'Create new data view', })} , ]} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index e236d7e8a1b89..8a38b0b026bbb 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -63,6 +63,8 @@ function getCompProps(): DiscoverSidebarProps { onEditRuntimeField: jest.fn(), editField: jest.fn(), viewMode: VIEW_MODE.DOCUMENT_LEVEL, + createNewDataView: jest.fn(), + onDataViewCreated: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 087a5a6ae312b..6569348f99038 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -69,6 +69,8 @@ export interface DiscoverSidebarProps extends Omit void; + createNewDataView: () => void; + /** * a statistics of the distribution of fields in the given hits */ @@ -104,6 +106,7 @@ export function DiscoverSidebarComponent({ closeFlyout, editField, viewMode, + createNewDataView, }: DiscoverSidebarProps) { const { uiSettings, dataViewFieldEditor } = useDiscoverServices(); const [fields, setFields] = useState(null); @@ -299,6 +302,7 @@ export function DiscoverSidebarComponent({ selectedIndexPattern={selectedIndexPattern} editField={editField} useNewFieldsApi={useNewFieldsApi} + createNewDataView={createNewDataView} /> @@ -336,6 +340,7 @@ export function DiscoverSidebarComponent({ selectedIndexPattern={selectedIndexPattern} useNewFieldsApi={useNewFieldsApi} editField={editField} + createNewDataView={createNewDataView} /> diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 1915f6707f94d..fdf11075e6b48 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -104,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps { trackUiMetric: jest.fn(), onEditRuntimeField: jest.fn(), viewMode: VIEW_MODE.DOCUMENT_LEVEL, + onDataViewCreated: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index abc59ff282863..88b43341afe7c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -102,6 +102,10 @@ export interface DiscoverSidebarResponsiveProps { * callback to execute on edit runtime field */ onEditRuntimeField: () => void; + /** + * callback to execute on create dataview + */ + onDataViewCreated: (dataView: DataView) => void; /** * Discover view mode */ @@ -115,7 +119,13 @@ export interface DiscoverSidebarResponsiveProps { */ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { const services = useDiscoverServices(); - const { selectedIndexPattern, onEditRuntimeField, useNewFieldsApi, onChangeIndexPattern } = props; + const { + selectedIndexPattern, + onEditRuntimeField, + useNewFieldsApi, + onChangeIndexPattern, + onDataViewCreated, + } = props; const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); /** @@ -146,12 +156,16 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) }, [selectedIndexPattern]); const closeFieldEditor = useRef<() => void | undefined>(); + const closeDataViewEditor = useRef<() => void | undefined>(); useEffect(() => { const cleanup = () => { if (closeFieldEditor?.current) { closeFieldEditor?.current(); } + if (closeDataViewEditor?.current) { + closeDataViewEditor?.current(); + } }; return () => { // Make sure to close the editor when unmounting @@ -163,11 +177,15 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) closeFieldEditor.current = ref; }, []); + const setDataViewEditorRef = useCallback((ref: () => void | undefined) => { + closeDataViewEditor.current = ref; + }, []); + const closeFlyout = useCallback(() => { setIsFlyoutVisible(false); }, []); - const { dataViewFieldEditor } = services; + const { dataViewFieldEditor, dataViewEditor } = services; const editField = useCallback( (fieldName?: string) => { @@ -203,6 +221,24 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) ] ); + const createNewDataView = useCallback(() => { + const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView; + if (!indexPatternFieldEditPermission) { + return; + } + const ref = dataViewEditor.openEditor({ + onSave: async (dataView) => { + onDataViewCreated(dataView); + }, + }); + if (setDataViewEditorRef) { + setDataViewEditorRef(ref); + } + if (closeFlyout) { + closeFlyout(); + } + }, [dataViewEditor, setDataViewEditorRef, closeFlyout, onDataViewCreated]); + if (!selectedIndexPattern) { return null; } @@ -218,6 +254,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) fieldCounts={fieldCounts.current} setFieldFilter={setFieldFilter} editField={editField} + createNewDataView={createNewDataView} /> )} @@ -244,6 +281,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) selectedIndexPattern={selectedIndexPattern} editField={editField} useNewFieldsApi={useNewFieldsApi} + createNewDataView={createNewDataView} /> @@ -307,6 +345,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) setFieldEditorRef={setFieldEditorRef} closeFlyout={closeFlyout} editField={editField} + createNewDataView={createNewDataView} />
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 393893432538b..f6492db6e8a42 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -40,6 +40,7 @@ import { FieldFormatsStart } from '../../field_formats/public'; import { EmbeddableStart } from '../../embeddable/public'; import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; +import { DataViewEditorStart } from '../../../plugins/data_view_editor/public'; export interface HistoryLocationState { referrer: string; @@ -68,6 +69,7 @@ export interface DiscoverServices { uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; dataViewFieldEditor: IndexPatternFieldEditorStart; + dataViewEditor: DataViewEditorStart; http: HttpStart; storage: Storage; spaces?: SpacesApi; @@ -109,5 +111,6 @@ export const buildServices = memoize(function ( dataViewFieldEditor: plugins.dataViewFieldEditor, http: core.http, spaces: plugins.spaces, + dataViewEditor: plugins.dataViewEditor, }; }); diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 43c03a59b5b25..e55158b0dad5e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -62,6 +62,7 @@ import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public import { FieldFormatsStart } from '../../field_formats/public'; import { injectTruncateStyles } from './utils/truncate_styles'; import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '../common'; +import { DataViewEditorStart } from '../../../plugins/data_view_editor/public'; import { useDiscoverServices } from './utils/use_discover_services'; declare module '../../share/public' { @@ -176,6 +177,7 @@ export interface DiscoverSetupPlugins { * @internal */ export interface DiscoverStartPlugins { + dataViewEditor: DataViewEditorStart; uiActions: UiActionsStart; embeddable: EmbeddableStart; navigation: NavigationStart; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 4ff6f0598d7d8..6dad573a272fb 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -25,6 +25,7 @@ { "path": "../data_view_field_editor/tsconfig.json"}, { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, - { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, + { "path": "../data_view_editor/tsconfig.json" } ] } diff --git a/test/functional/apps/discover/_data_view_editor.ts b/test/functional/apps/discover/_data_view_editor.ts new file mode 100644 index 0000000000000..c67964fddf93b --- /dev/null +++ b/test/functional/apps/discover/_data_view_editor.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from './ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { + defaultIndex: 'logstash-*', + }; + + const createDataView = async (dataViewName: string) => { + await PageObjects.discover.clickIndexPatternActions(); + await PageObjects.discover.clickCreateNewDataView(); + await testSubjects.setValue('createIndexPatternNameInput', dataViewName, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + await testSubjects.click('saveIndexPatternButton'); + }; + + describe('discover integration with data view editor', function describeIndexTests() { + before(async function () { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); + }); + + it('allows creating a new data view', async function () { + const dataViewToCreate = 'logstash'; + await createDataView(dataViewToCreate); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitForWithTimeout( + 'data view selector to include a newly created dataview', + 5000, + async () => { + const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView(); + // data view editor will add wildcard symbol by default + // so we need to include it in our original title when comparing + return dataViewTitle === `${dataViewToCreate}*`; + } + ); + }); + }); +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index b5eb160526876..d2b627c175fcc 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -54,6 +54,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_search_on_page_load')); loadTestFile(require.resolve('./_chart_hidden')); loadTestFile(require.resolve('./_context_encoded_url_param')); + loadTestFile(require.resolve('./_data_view_editor')); loadTestFile(require.resolve('./_empty_state')); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index f9328e89cd19e..effacb30bdc89 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -363,6 +363,13 @@ export class DiscoverPageObject extends FtrService { }); } + public async clickCreateNewDataView() { + await this.retry.try(async () => { + await this.testSubjects.click('dataview-create-new'); + await this.find.byClassName('indexPatternEditor__form'); + }); + } + public async hasNoResults() { return await this.testSubjects.exists('discoverNoResults'); } @@ -598,4 +605,10 @@ export class DiscoverPageObject extends FtrService { await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent'); }); } + + public async getCurrentlySelectedDataView() { + await this.testSubjects.existOrFail('discover-sidebar'); + const button = await this.testSubjects.find('indexPattern-switch-link'); + return button.getAttribute('title'); + } }