[Discover] Create data view from sidebar (#123391)

* [Discover] Create data view from sidebar

* Fix failing unit test

* Fix invalid import

* Addressing PR comments

* Add horizontal separator

* Design tweaks

* Update unit test

* Remove double declaration

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ryan Keairns <contactryank@gmail.com>
This commit is contained in:
Maja Grubic 2022-01-31 16:03:22 +01:00 committed by GitHub
parent 6a707516f6
commit 9b20c4f035
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 194 additions and 13 deletions

View file

@ -13,7 +13,8 @@
"navigation",
"uiActions",
"savedObjects",
"dataViewFieldEditor"
"dataViewFieldEditor",
"dataViewEditor"
],
"optionalPlugins": ["home", "share", "usageCollection", "spaces"],
"requiredBundles": ["kibanaUtils", "home", "kibanaReact", "dataViews"],

View file

@ -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 (
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
@ -245,6 +253,7 @@ export function DiscoverLayout({
useNewFieldsApi={useNewFieldsApi}
onEditRuntimeField={onEditRuntimeField}
viewMode={viewMode}
onDataViewCreated={onDataViewCreated}
/>
</EuiFlexItem>
<EuiHideFor sizes={['xs', 's']}>

View file

@ -133,6 +133,7 @@ exports[`Discover DataView Management renders correctly 1`] = `
}
>
<DiscoverIndexPatternManagement
createNewDataView={[MockFunction]}
editField={[MockFunction]}
selectedIndexPattern={
DataView {
@ -685,7 +686,7 @@ exports[`Discover DataView Management renders correctly 1`] = `
hasArrow={true}
isOpen={false}
ownFocus={true}
panelPaddingSize="s"
panelPaddingSize="none"
>
<div
className="euiPopover euiPopover--anchorDownCenter"

View file

@ -54,6 +54,7 @@ describe('Discover DataView Management', () => {
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}
/>
</KibanaContextProvider>
);
@ -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();
});
});

View file

@ -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 (
<EuiPopover
panelPaddingSize="s"
panelPaddingSize="none"
isOpen={isAddIndexPatternFieldPopoverOpen}
closePopover={() => {
setIsAddIndexPatternFieldPopoverOpen(false);
@ -67,7 +78,8 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
}
>
<EuiContextMenuPanel
size="s"
size="m"
title="Data view"
items={[
<EuiContextMenuItem
key="add"
@ -79,7 +91,7 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
}}
>
{i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', {
defaultMessage: 'Add field to data view',
defaultMessage: 'Add field',
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
@ -94,7 +106,21 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
}}
>
{i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', {
defaultMessage: 'Manage data view fields',
defaultMessage: 'Manage settings',
})}
</EuiContextMenuItem>,
<EuiHorizontalRule style={{ margin: '0px' }} />,
<EuiContextMenuItem
key="new"
icon="plusInCircleFilled"
data-test-subj="dataview-create-new"
onClick={() => {
setIsAddIndexPatternFieldPopoverOpen(false);
createNewDataView();
}}
>
{i18n.translate('discover.fieldChooser.dataViews.createNewDataView', {
defaultMessage: 'Create new data view',
})}
</EuiContextMenuItem>,
]}

View file

@ -63,6 +63,8 @@ function getCompProps(): DiscoverSidebarProps {
onEditRuntimeField: jest.fn(),
editField: jest.fn(),
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
createNewDataView: jest.fn(),
onDataViewCreated: jest.fn(),
};
}

View file

@ -69,6 +69,8 @@ export interface DiscoverSidebarProps extends Omit<DiscoverSidebarResponsiveProp
editField: (fieldName?: string) => 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<DataViewField[] | null>(null);
@ -299,6 +302,7 @@ export function DiscoverSidebarComponent({
selectedIndexPattern={selectedIndexPattern}
editField={editField}
useNewFieldsApi={useNewFieldsApi}
createNewDataView={createNewDataView}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -336,6 +340,7 @@ export function DiscoverSidebarComponent({
selectedIndexPattern={selectedIndexPattern}
useNewFieldsApi={useNewFieldsApi}
editField={editField}
createNewDataView={createNewDataView}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -104,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
trackUiMetric: jest.fn(),
onEditRuntimeField: jest.fn(),
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
onDataViewCreated: jest.fn(),
};
}

View file

@ -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}
/>
</EuiHideFor>
)}
@ -244,6 +281,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
selectedIndexPattern={selectedIndexPattern}
editField={editField}
useNewFieldsApi={useNewFieldsApi}
createNewDataView={createNewDataView}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -307,6 +345,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
setFieldEditorRef={setFieldEditorRef}
closeFlyout={closeFlyout}
editField={editField}
createNewDataView={createNewDataView}
/>
</div>
</EuiFlyout>

View file

@ -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,
};
});

View file

@ -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;

View file

@ -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" }
]
}

View file

@ -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}*`;
}
);
});
});
}

View file

@ -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'));
});
}

View file

@ -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');
}
}