diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx index c00263698d3f0..d12ac172b72cd 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx @@ -125,11 +125,11 @@ const datasourceMap = mockDatasourceMap(); const visualizationMap = mockVisualizationMap(); describe('LensEditConfigurationFlyout', () => { - function renderConfigFlyout( + async function renderConfigFlyout( propsOverrides: Partial = {}, query?: Query | AggregateQuery ) { - return renderWithReduxStore( + const { container, ...rest } = renderWithReduxStore( { }, } ); + await waitFor(() => container.querySelector('lnsEditFlyoutBody')); + return { container, ...rest }; } it('should display the header and the link to editor if necessary props are given', async () => { const navigateToLensEditorSpy = jest.fn(); - - renderConfigFlyout({ + await renderConfigFlyout({ displayFlyoutHeader: true, navigateToLensEditor: navigateToLensEditorSpy, }); @@ -170,7 +171,7 @@ describe('LensEditConfigurationFlyout', () => { }); it('should display the header title correctly for a newly created panel', async () => { - renderConfigFlyout({ + await renderConfigFlyout({ displayFlyoutHeader: true, isNewPanel: true, }); @@ -182,7 +183,7 @@ describe('LensEditConfigurationFlyout', () => { it('should call the closeFlyout callback if cancel button is clicked', async () => { const closeFlyoutSpy = jest.fn(); - renderConfigFlyout({ + await renderConfigFlyout({ closeFlyout: closeFlyoutSpy, }); expect(screen.getByTestId('lns-layerPanel-0')).toBeInTheDocument(); @@ -192,7 +193,7 @@ describe('LensEditConfigurationFlyout', () => { it('should call the updatePanelState callback if cancel button is clicked', async () => { const updatePanelStateSpy = jest.fn(); - renderConfigFlyout({ + await renderConfigFlyout({ updatePanelState: updatePanelStateSpy, }); expect(screen.getByTestId('lns-layerPanel-0')).toBeInTheDocument(); @@ -203,7 +204,7 @@ describe('LensEditConfigurationFlyout', () => { it('should call the updateByRefInput callback if cancel button is clicked and savedObjectId exists', async () => { const updateByRefInputSpy = jest.fn(); - renderConfigFlyout({ + await renderConfigFlyout({ closeFlyout: jest.fn(), updateByRefInput: updateByRefInputSpy, savedObjectId: 'id', @@ -216,7 +217,7 @@ describe('LensEditConfigurationFlyout', () => { const updateByRefInputSpy = jest.fn(); const saveByRefSpy = jest.fn(); - renderConfigFlyout({ + await renderConfigFlyout({ closeFlyout: jest.fn(), updateByRefInput: updateByRefInputSpy, savedObjectId: 'id', @@ -230,7 +231,7 @@ describe('LensEditConfigurationFlyout', () => { it('should call the onApplyCb callback if apply button is clicked', async () => { const onApplyCbSpy = jest.fn(); - renderConfigFlyout( + await renderConfigFlyout( { closeFlyout: jest.fn(), onApplyCb: onApplyCbSpy, @@ -254,14 +255,14 @@ describe('LensEditConfigurationFlyout', () => { }); it('should not display the editor if canEditTextBasedQuery prop is false', async () => { - renderConfigFlyout({ + await renderConfigFlyout({ canEditTextBasedQuery: false, }); expect(screen.queryByTestId('TextBasedLangEditor')).toBeNull(); }); it('should not display the editor if canEditTextBasedQuery prop is true but the query is not text based', async () => { - renderConfigFlyout({ + await renderConfigFlyout({ canEditTextBasedQuery: true, attributes: { ...lensAttributes, @@ -278,14 +279,14 @@ describe('LensEditConfigurationFlyout', () => { }); it('should not display the suggestions if hidesSuggestions prop is true', async () => { - renderConfigFlyout({ + await renderConfigFlyout({ hidesSuggestions: true, }); expect(screen.queryByTestId('InlineEditingSuggestions')).toBeNull(); }); it('should display the suggestions if canEditTextBasedQuery prop is true', async () => { - renderConfigFlyout( + await renderConfigFlyout( { canEditTextBasedQuery: true, }, @@ -298,7 +299,7 @@ describe('LensEditConfigurationFlyout', () => { }); it('should display the ES|QL results table if canEditTextBasedQuery prop is true', async () => { - renderConfigFlyout({ + await renderConfigFlyout({ canEditTextBasedQuery: true, }); await waitFor(() => expect(screen.getByTestId('ESQLQueryResults')).toBeInTheDocument()); @@ -317,7 +318,7 @@ describe('LensEditConfigurationFlyout', () => { // todo: replace testDatasource with formBased or textBased as it's the only ones accepted // @ts-ignore newProps.attributes.state.datasourceStates.testDatasource = 'state'; - renderConfigFlyout(newProps); + await renderConfigFlyout(newProps); expect(screen.getByRole('button', { name: /apply changes/i })).toBeDisabled(); }); it('save button should be disabled if expression cannot be generated', async () => { @@ -337,7 +338,7 @@ describe('LensEditConfigurationFlyout', () => { }, }; - renderConfigFlyout(newProps); + await renderConfigFlyout(newProps); expect(screen.getByRole('button', { name: /apply changes/i })).toBeDisabled(); }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx index aab6b57840f65..c8fec4184190d 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx @@ -6,37 +6,57 @@ */ import React from 'react'; -import ReactDOM from 'react-dom'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { - dataViewPluginMocks, - Start as DataViewPublicStart, -} from '@kbn/data-views-plugin/public/mocks'; -import { InnerFormBasedDataPanel, FormBasedDataPanel } from './datapanel'; -import { FieldListGrouped } from '@kbn/unified-field-list'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { render, screen, within } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import userEvent from '@testing-library/user-event'; +import { FormBasedDataPanel, FormBasedDataPanelProps } from './datapanel'; import * as UseExistingFieldsApi from '@kbn/unified-field-list/src/hooks/use_existing_fields'; import * as ExistingFieldsServiceApi from '@kbn/unified-field-list/src/services/field_existing/load_field_existing'; -import { FieldItem } from '../common/field_item'; import { act } from 'react-dom/test-utils'; import { coreMock } from '@kbn/core/public/mocks'; import { FormBasedPrivateState } from './types'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiCallOut, EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-plugin/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; -import { TermsIndexPatternColumn } from './operations'; -import { DOCUMENT_FIELD_NAME } from '../../../common/constants'; import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock'; import { createMockFramePublicAPI } from '../../mocks'; import { DataViewsState } from '../../state_management'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { UI_SETTINGS } from '@kbn/data-plugin/public'; -import { ReactWrapper } from 'enzyme'; -import { IndexPatternField } from '../../types'; + +const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + +jest.spyOn(UseExistingFieldsApi, 'useExistingFieldsFetcher'); +jest.spyOn(UseExistingFieldsApi, 'useExistingFieldsReader'); + +const loadFieldExistingMock = jest.spyOn(ExistingFieldsServiceApi, 'loadFieldExisting'); +loadFieldExistingMock.mockResolvedValue({ + indexPatternTitle: 'idx1', + existingFieldNames: [], +}); + +function getFrameAPIMock({ + indexPatterns, + ...rest +}: Partial & { indexPatterns: DataViewsState['indexPatterns'] }) { + const frameAPI = createMockFramePublicAPI(); + + return { + ...frameAPI, + dataViews: { + ...frameAPI.dataViews, + indexPatterns, + ...rest, + }, + }; +} + +const dslQuery = { bool: { must: [], filter: [], should: [], must_not: [] } }; const fieldsOne = [ { @@ -137,407 +157,222 @@ const fieldsTwo = [ documentField, ]; -const fieldsThree = [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, +const indexPatterns = { + '1': { + id: '1', + title: 'idx1', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), + isPersisted: true, + spec: {}, }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, + '2': { + id: '2', + title: 'idx2', + timeFieldName: 'timestamp', + hasRestrictions: true, + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), + isPersisted: true, + spec: {}, }, - documentField, -]; +}; -jest.spyOn(UseExistingFieldsApi, 'useExistingFieldsFetcher'); -jest.spyOn(UseExistingFieldsApi, 'useExistingFieldsReader'); -jest.spyOn(ExistingFieldsServiceApi, 'loadFieldExisting').mockImplementation(async () => ({ - indexPatternTitle: 'test', - existingFieldNames: [], -})); +const core = coreMock.createStart(); +const dataViews = dataViewPluginMocks.createStartContract(); -const initialState: FormBasedPrivateState = { - currentIndexPatternId: '1', +const constructState = (currentIndexPatternId: string): FormBasedPrivateState => ({ + currentIndexPatternId, layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - label: 'My Op', - dataType: 'string', - isBucketed: true, - operationType: 'terms', - sourceField: 'source', - params: { - size: 5, - orderDirection: 'asc', - orderBy: { - type: 'alphabetical', - }, - }, - } as TermsIndexPatternColumn, - col2: { - label: 'My Op', - dataType: 'number', - isBucketed: false, - operationType: 'average', - sourceField: 'memory', - }, - }, - }, - second: { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - label: 'My Op', - dataType: 'string', - isBucketed: true, - operationType: 'terms', - sourceField: 'source', - params: { - size: 5, - orderDirection: 'asc', - orderBy: { - type: 'alphabetical', - }, - }, - } as TermsIndexPatternColumn, - col2: { - label: 'My Op', - dataType: 'number', - isBucketed: false, - operationType: 'average', - sourceField: 'bytes', - }, - }, + 1: { + indexPatternId: currentIndexPatternId, + columnOrder: [], + columns: {}, }, }, +}); + +const frame = getFrameAPIMock({ indexPatterns }); +const defaultProps = { + data: dataPluginMock.createStartContract(), + dataViews, + fieldFormats: fieldFormatsServiceMock.createStartContract(), + indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), + indexPatternService: createIndexPatternServiceMock({ + updateIndexPatterns: jest.fn(), + core, + dataViews, + }), + onIndexPatternRefresh: jest.fn(), + core, + dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, + charts: chartPluginMock.createSetupContract(), + query: { query: '', language: 'lucene' }, + filters: [], + showNoDataPopover: jest.fn(), + dropOntoWorkspace: jest.fn(), + hasSuggestionForField: jest.fn(() => false), + uiActions: uiActionsPluginMock.createStartContract(), + frame, + activeIndexPatterns: [frame.dataViews.indexPatterns['1']], + setState: jest.fn(), + state: constructState('1'), }; -function getFrameAPIMock({ - indexPatterns, - ...rest -}: Partial & { indexPatterns: DataViewsState['indexPatterns'] }) { - const frameAPI = createMockFramePublicAPI(); +core.uiSettings.get.mockImplementation((key: string) => { + if (key === UI_SETTINGS.META_FIELDS) { + return []; + } +}); + +dataViews.get.mockImplementation(async (id: string) => { + const dataView = [indexPatterns['1'], indexPatterns['2']].find( + (indexPattern) => indexPattern.id === id + ) as unknown as DataView; + dataView.metaFields = ['_id']; + return dataView; +}); + +const waitToLoad = async () => + await act(async () => new Promise((resolve) => setTimeout(resolve, 0))); +const renderFormBasedDataPanel = async (propsOverrides?: Partial) => { + const { rerender, ...rest } = render( + , + { + wrapper: ({ children }) => {children}, + } + ); + await waitToLoad(); return { - ...frameAPI, - dataViews: { - ...frameAPI.dataViews, - indexPatterns, - ...rest, + ...rest, + rerender: async (overrides?: Partial) => { + rerender(); + await waitToLoad(); }, }; -} - -const dslQuery = { bool: { must: [], filter: [], should: [], must_not: [] } }; - -// @ts-expect-error Portal mocks are notoriously difficult to type -ReactDOM.createPortal = jest.fn((element) => element); - -async function mountAndWaitForLazyModules(component: React.ReactElement): Promise { - let inst: ReactWrapper; - await act(async () => { - inst = await mountWithIntl(component); - // wait for lazy modules - await new Promise((resolve) => setTimeout(resolve, 0)); - await inst.update(); - }); +}; - return inst!; -} +const getAriaDescription = () => screen.getByTestId('lnsIndexPattern__ariaDescription').textContent; -// TODO: After the i18n upgrade it seem that some underlying error in these tests surfaced: -// | TypeError: Cannot read properties of null (reading 'tag') -// Does not seem related to the i18n upgrade -describe.skip('FormBased Data Panel', () => { - const indexPatterns = { - a: { - id: 'a', - title: 'aaa', - timeFieldName: 'atime', - fields: fieldsOne, - getFieldByName: getFieldByNameFactory(fieldsOne), - hasRestrictions: false, - isPersisted: true, - spec: {}, - }, - b: { - id: 'b', - title: 'bbb', - timeFieldName: 'btime', - fields: fieldsTwo, - getFieldByName: getFieldByNameFactory(fieldsTwo), - hasRestrictions: false, - isPersisted: true, - spec: {}, - }, - }; +const getFieldNames = (container = 'fieldList') => { + return [...within(screen.getByTestId(container)).queryAllByTestId('lnsFieldListPanelField')].map( + (el) => el.querySelector('.kbnFieldButton__nameInner')?.textContent + ); +}; - const defaultIndexPatterns = { - '1': { - id: '1', - title: 'idx1', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: fieldsOne, - getFieldByName: getFieldByNameFactory(fieldsOne), - isPersisted: true, - spec: {}, - }, - '2': { - id: '2', - title: 'idx2', - timeFieldName: 'timestamp', - hasRestrictions: true, - fields: fieldsTwo, - getFieldByName: getFieldByNameFactory(fieldsTwo), - isPersisted: true, - spec: {}, - }, - '3': { - id: '3', - title: 'idx3', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: fieldsThree, - getFieldByName: getFieldByNameFactory(fieldsThree), - isPersisted: true, - spec: {}, - }, - }; +const getSelectedFieldsNames = () => getFieldNames('lnsIndexPatternSelectedFields'); +const getAvailableFieldsNames = () => getFieldNames('lnsIndexPatternAvailableFields'); +const getEmptyFieldsNames = () => getFieldNames('lnsIndexPatternEmptyFields'); +const getMetaFieldsNames = () => getFieldNames('lnsIndexPatternMetaFields'); - let defaultProps: Parameters[0] & { - showNoDataPopover: () => void; - }; - let core: ReturnType<(typeof coreMock)['createStart']>; - let dataViews: DataViewPublicStart; +const searchForPhrase = async (phrase: string) => { + jest.useFakeTimers(); + await user.type(screen.getByRole('searchbox', { name: 'Search field names' }), phrase); + act(() => jest.advanceTimersByTime(256)); + jest.useRealTimers(); +}; +describe('FormBased Data Panel', () => { beforeEach(() => { - core = coreMock.createStart(); - dataViews = dataViewPluginMocks.createStartContract(); - const frame = getFrameAPIMock({ indexPatterns: defaultIndexPatterns }); - defaultProps = { - data: dataPluginMock.createStartContract(), - dataViews, - fieldFormats: fieldFormatsServiceMock.createStartContract(), - indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), - onIndexPatternRefresh: jest.fn(), - currentIndexPatternId: '1', - core, - dateRange: { - fromDate: 'now-7d', - toDate: 'now', - }, - charts: chartPluginMock.createSetupContract(), - query: { query: '', language: 'lucene' }, - filters: [], - showNoDataPopover: jest.fn(), - dropOntoWorkspace: jest.fn(), - hasSuggestionForField: jest.fn(() => false), - uiActions: uiActionsPluginMock.createStartContract(), - indexPatternService: createIndexPatternServiceMock({ core, dataViews }), - frame, - activeIndexPatterns: [frame.dataViews.indexPatterns['1']], - }; - - core.uiSettings.get.mockImplementation((key: string) => { - if (key === UI_SETTINGS.META_FIELDS) { - return []; - } - }); - dataViews.get.mockImplementation(async (id: string) => { - const dataView = [ - indexPatterns.a, - indexPatterns.b, - defaultIndexPatterns['1'], - defaultIndexPatterns['2'], - defaultIndexPatterns['3'], - ].find((indexPattern) => indexPattern.id === id) as unknown as DataView; - dataView.metaFields = ['_id']; - return dataView; - }); - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockClear(); - (UseExistingFieldsApi.useExistingFieldsReader as jest.Mock).mockClear(); - (UseExistingFieldsApi.useExistingFieldsFetcher as jest.Mock).mockClear(); + jest.clearAllMocks(); UseExistingFieldsApi.resetExistingFieldsCache(); window.localStorage.removeItem('lens.unifiedFieldList.initiallyOpenSections'); }); it('should render a warning if there are no index patterns', async () => { - const wrapper = await mountAndWaitForLazyModules( - - ); - expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]').exists()).toBeTruthy(); + await renderFormBasedDataPanel({ + state: { + layers: {}, + currentIndexPatternId: '', + }, + frame: createMockFramePublicAPI(), + }); + + expect(screen.getByTestId('indexPattern-no-indexpatterns')).toBeInTheDocument(); }); describe('loading existence data', () => { - function testProps({ - currentIndexPatternId, - otherProps, - }: { - currentIndexPatternId: keyof typeof indexPatterns; - otherProps?: object; - }) { - return { - ...defaultProps, - indexPatternService: createIndexPatternServiceMock({ - updateIndexPatterns: jest.fn(), - core, - dataViews, - }), - setState: jest.fn(), - dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, - frame: getFrameAPIMock({ - indexPatterns: indexPatterns as unknown as DataViewsState['indexPatterns'], - }), - state: { - currentIndexPatternId, - layers: { - 1: { - indexPatternId: currentIndexPatternId, - columnOrder: [], - columns: {}, - }, - }, - } as FormBasedPrivateState, - ...(otherProps || {}), - }; - } - it('loads existence data', async () => { - const props = testProps({ - currentIndexPatternId: 'a', - }); - - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [indexPatterns.a.fields[0].name, indexPatterns.a.fields[1].name], - }; + loadFieldExistingMock.mockResolvedValue({ + indexPatternTitle: 'idx1', + existingFieldNames: [indexPatterns['1'].fields[0].name, indexPatterns['1'].fields[1].name], }); - const inst = await mountAndWaitForLazyModules(); + await renderFormBasedDataPanel(); expect(UseExistingFieldsApi.useExistingFieldsFetcher).toHaveBeenCalledWith( expect.objectContaining({ - dataViews: [indexPatterns.a], - query: props.query, - filters: props.filters, - fromDate: props.dateRange.fromDate, - toDate: props.dateRange.toDate, + dataViews: [indexPatterns['1']], + query: defaultProps.query, + filters: defaultProps.filters, + fromDate: defaultProps.dateRange.fromDate, + toDate: defaultProps.dateRange.toDate, }) ); expect(UseExistingFieldsApi.useExistingFieldsReader).toHaveBeenCalled(); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '2 available fields. 3 empty fields. 0 meta fields.' - ); + expect(getAriaDescription()).toBe('2 available fields. 3 empty fields. 0 meta fields.'); }); it('loads existence data for current index pattern id', async () => { - const props = testProps({ - currentIndexPatternId: 'b', + loadFieldExistingMock.mockResolvedValue({ + indexPatternTitle: 'idx1', + existingFieldNames: [indexPatterns['2'].fields[0].name], }); - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [indexPatterns.b.fields[0].name], - }; + await renderFormBasedDataPanel({ + state: constructState('2'), }); - const inst = await mountAndWaitForLazyModules(); - expect(UseExistingFieldsApi.useExistingFieldsFetcher).toHaveBeenCalledWith( expect.objectContaining({ - dataViews: [indexPatterns.b], - query: props.query, - filters: props.filters, - fromDate: props.dateRange.fromDate, - toDate: props.dateRange.toDate, + dataViews: [indexPatterns['2']], + query: defaultProps.query, + filters: defaultProps.filters, + fromDate: defaultProps.dateRange.fromDate, + toDate: defaultProps.dateRange.toDate, }) ); expect(UseExistingFieldsApi.useExistingFieldsReader).toHaveBeenCalled(); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '1 available field. 2 empty fields. 0 meta fields.' - ); + expect(getAriaDescription()).toBe('1 available field. 2 empty fields. 0 meta fields.'); }); it('does not load existence data if date and index pattern ids are unchanged', async () => { - const props = testProps({ - currentIndexPatternId: 'a', - otherProps: { - dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, - }, + loadFieldExistingMock.mockResolvedValue({ + indexPatternTitle: 'idx1', + existingFieldNames: [indexPatterns['1'].fields[0].name, indexPatterns['1'].fields[1].name], }); - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [indexPatterns.a.fields[0].name, indexPatterns.a.fields[1].name], - }; - }); - - const inst = await mountAndWaitForLazyModules(); + const { rerender } = await renderFormBasedDataPanel(); expect(UseExistingFieldsApi.useExistingFieldsFetcher).toHaveBeenCalledWith( expect.objectContaining({ - dataViews: [indexPatterns.a], - fromDate: props.dateRange.fromDate, - toDate: props.dateRange.toDate, + dataViews: [indexPatterns['1']], + fromDate: defaultProps.dateRange.fromDate, + toDate: defaultProps.dateRange.toDate, }) ); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(1); - await act(async () => { - await inst.setProps({ dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' } }); - await inst.update(); - }); - + await rerender(); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(1); }); it('loads existence data if date range changes', async () => { - const props = testProps({ - currentIndexPatternId: 'a', - otherProps: { - dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, - }, - }); - - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [indexPatterns.a.fields[0].name, indexPatterns.a.fields[1].name], - }; + loadFieldExistingMock.mockResolvedValue({ + indexPatternTitle: 'idx1', + existingFieldNames: [indexPatterns['1'].fields[0].name, indexPatterns['1'].fields[1].name], }); - const inst = await mountAndWaitForLazyModules(); + const { rerender } = await renderFormBasedDataPanel(); expect(UseExistingFieldsApi.useExistingFieldsFetcher).toHaveBeenCalledWith( expect.objectContaining({ - dataViews: [indexPatterns.a], - fromDate: props.dateRange.fromDate, - toDate: props.dateRange.toDate, + dataViews: [indexPatterns['1']], + fromDate: defaultProps.dateRange.fromDate, + toDate: defaultProps.dateRange.toDate, }) ); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(1); @@ -546,15 +381,11 @@ describe.skip('FormBased Data Panel', () => { fromDate: '2019-01-01', toDate: '2020-01-01', dslQuery, - dataView: indexPatterns.a, - timeFieldName: indexPatterns.a.timeFieldName, + dataView: indexPatterns['1'], + timeFieldName: indexPatterns['1'].timeFieldName, }) ); - - await act(async () => { - await inst.setProps({ dateRange: { fromDate: '2019-01-01', toDate: '2020-01-02' } }); - await inst.update(); - }); + await rerender({ dateRange: { fromDate: '2019-01-01', toDate: '2020-01-02' } }); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(2); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledWith( @@ -562,38 +393,30 @@ describe.skip('FormBased Data Panel', () => { fromDate: '2019-01-01', toDate: '2020-01-02', dslQuery, - dataView: indexPatterns.a, - timeFieldName: indexPatterns.a.timeFieldName, + dataView: indexPatterns['1'], + timeFieldName: indexPatterns['1'].timeFieldName, }) ); }); it('loads existence data if layer index pattern changes', async () => { - const props = testProps({ - currentIndexPatternId: 'a', - otherProps: { - dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, - }, - }); - - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation( - async ({ dataView }) => { - return { - existingFieldNames: - dataView === indexPatterns.a - ? [indexPatterns.a.fields[0].name, indexPatterns.a.fields[1].name] - : [indexPatterns.b.fields[0].name], - }; - } - ); - - const inst = await mountAndWaitForLazyModules(); + jest + .spyOn(ExistingFieldsServiceApi, 'loadFieldExisting') + .mockImplementation(async ({ dataView }) => ({ + indexPatternTitle: 'idx1', + existingFieldNames: + dataView.id === indexPatterns['1'].id + ? [indexPatterns['1'].fields[0].name, indexPatterns['1'].fields[1].name] + : [indexPatterns['2'].fields[0].name], + })); + + const { rerender } = await renderFormBasedDataPanel(); expect(UseExistingFieldsApi.useExistingFieldsFetcher).toHaveBeenCalledWith( expect.objectContaining({ - dataViews: [indexPatterns.a], - fromDate: props.dateRange.fromDate, - toDate: props.dateRange.toDate, + dataViews: [indexPatterns['1']], + fromDate: defaultProps.dateRange.fromDate, + toDate: defaultProps.dateRange.toDate, }) ); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(1); @@ -602,31 +425,14 @@ describe.skip('FormBased Data Panel', () => { fromDate: '2019-01-01', toDate: '2020-01-01', dslQuery, - dataView: indexPatterns.a, - timeFieldName: indexPatterns.a.timeFieldName, + dataView: indexPatterns['1'], + timeFieldName: indexPatterns['1'].timeFieldName, }) ); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '2 available fields. 3 empty fields. 0 meta fields.' - ); + expect(getAriaDescription()).toBe('2 available fields. 3 empty fields. 0 meta fields.'); - await act(async () => { - await inst.setProps({ - currentIndexPatternId: 'b', - state: { - currentIndexPatternId: 'b', - layers: { - 1: { - indexPatternId: 'b', - columnOrder: [], - columns: {}, - }, - }, - } as FormBasedPrivateState, - }); - await inst.update(); - }); + await rerender({ state: constructState('2') }); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(2); expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledWith( @@ -634,409 +440,243 @@ describe.skip('FormBased Data Panel', () => { fromDate: '2019-01-01', toDate: '2020-01-01', dslQuery, - dataView: indexPatterns.b, - timeFieldName: indexPatterns.b.timeFieldName, + dataView: indexPatterns['2'], + timeFieldName: indexPatterns['2'].timeFieldName, }) ); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '1 available field. 2 empty fields. 0 meta fields.' - ); + expect(getAriaDescription()).toBe('1 available field. 2 empty fields. 0 meta fields.'); }); it('shows a loading indicator when loading', async () => { - const props = testProps({ - currentIndexPatternId: 'b', - }); - - let resolveFunction: (arg: unknown) => void; - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockReset(); - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(() => { - return new Promise((resolve) => { + let resolveFunction: (arg: { + existingFieldNames: string[]; + indexPatternTitle: string; + }) => void; + loadFieldExistingMock.mockResolvedValue( + new Promise((resolve) => { resolveFunction = resolve; - }); - }); - const inst = await mountAndWaitForLazyModules(); - - expect(inst.find(EuiProgress).length).toEqual(1); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '' + }) ); - await act(async () => { - resolveFunction!({ - existingFieldNames: [indexPatterns.b.fields[0].name], - }); - await inst.update(); - }); + await renderFormBasedDataPanel({ state: constructState('2') }); + expect(getAriaDescription()).toBe(''); + expect(screen.getByTestId('fieldListLoading')).toBeInTheDocument(); - await act(async () => { - await inst.update(); + await resolveFunction!({ + existingFieldNames: [indexPatterns['2'].fields[0].name], + indexPatternTitle: 'idx1', }); - expect(inst.find(EuiProgress).length).toEqual(0); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '1 available field. 2 empty fields. 0 meta fields.' - ); + await waitToLoad(); + + expect(getAriaDescription()).toBe('1 available field. 2 empty fields. 0 meta fields.'); + expect(screen.queryByTestId('fieldListLoading')).not.toBeInTheDocument(); }); it("should trigger showNoDataPopover if fields don't have data", async () => { - const props = testProps({ - currentIndexPatternId: 'a', - }); - - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [], - }; + loadFieldExistingMock.mockResolvedValue({ + existingFieldNames: [], + indexPatternTitle: 'idx1', }); - const inst = await mountAndWaitForLazyModules(); + await renderFormBasedDataPanel(); expect(defaultProps.showNoDataPopover).toHaveBeenCalled(); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '0 available fields. 5 empty fields. 0 meta fields.' - ); + expect(getAriaDescription()).toBe('0 available fields. 5 empty fields. 0 meta fields.'); }); it("should default to empty dsl if query can't be parsed", async () => { - const props = testProps({ - currentIndexPatternId: 'a', - otherProps: { - query: { - language: 'kuery', - query: '@timestamp : NOT *', - }, - }, + loadFieldExistingMock.mockResolvedValue({ + indexPatternTitle: 'idx1', + existingFieldNames: [indexPatterns['1'].fields[0].name, indexPatterns['1'].fields[1].name], }); - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [indexPatterns.a.fields[0].name, indexPatterns.a.fields[1].name], - }; + await renderFormBasedDataPanel({ + query: { language: 'kuery', query: '@timestamp : NOT *' }, }); - const inst = await mountAndWaitForLazyModules(); - expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledWith( - expect.objectContaining({ - dslQuery: { - bool: { - must_not: { - match_all: {}, - }, - }, - }, - }) + expect.objectContaining({ dslQuery: { bool: { must_not: { match_all: {} } } } }) ); - expect(inst.find('[data-test-subj="lnsIndexPattern__ariaDescription"]').first().text()).toBe( - '2 available fields. 3 empty fields. 0 meta fields.' - ); + expect(getAriaDescription()).toBe('2 available fields. 3 empty fields. 0 meta fields.'); }); }); describe('displaying field list', () => { - let props: Parameters[0]; beforeEach(() => { - props = { - ...defaultProps, - }; - - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: ['bytes', 'memory'], - }; + loadFieldExistingMock.mockResolvedValue({ + existingFieldNames: ['bytes', 'memory'], + indexPatternTitle: 'idx1', }); }); it('should list all selected fields if exist', async () => { - const newProps = { - ...props, - layerFields: ['bytes'], - }; - - const wrapper = await mountAndWaitForLazyModules(); - - expect( - wrapper - .find('[data-test-subj="lnsIndexPatternSelectedFields"]') - .find(FieldItem) - .map((fieldItem) => fieldItem.prop('field').name) - ).toEqual(['bytes']); + await renderFormBasedDataPanel({ layerFields: ['bytes'] }); + expect(getSelectedFieldsNames()).toEqual(['bytes']); }); it('should not list the selected fields accordion if no fields given', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - expect( - wrapper! - .find('[data-test-subj="lnsIndexPatternSelectedFields"]') - .find(FieldItem) - .map((fieldItem) => fieldItem.prop('field').name) - ).toEqual([]); + await renderFormBasedDataPanel(); + expect(screen.queryByTestId('lnsIndexPatternSelectedFields')).not.toBeInTheDocument(); }); it('should list all supported fields in the pattern sorted alphabetically in groups', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - expect(wrapper.find(FieldItem).first().prop('field')).toEqual( - expect.objectContaining({ - displayName: 'Records', - }) - ); - const availableAccordion = wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]'); - expect( - availableAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name) - ).toEqual(['memory', 'bytes']); - expect(availableAccordion.find(FieldItem).at(0).prop('exists')).toEqual(true); - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"]') - .find('button') - .first() - .simulate('click'); - const emptyAccordion = wrapper.find('[data-test-subj="lnsIndexPatternEmptyFields"]'); - expect( - emptyAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name) - ).toEqual(['client', 'source', 'timestamp']); - expect( - emptyAccordion - .find(FieldItem) - .map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName) - ).toEqual(['client', 'source', 'timestampLabel']); - expect(emptyAccordion.find(FieldItem).at(1).prop('exists')).toEqual(false); + const { container } = await renderFormBasedDataPanel(); + expect(container.querySelector('.kbnFieldButton__nameInner')).toHaveTextContent('Records'); + expect(getAvailableFieldsNames()).toEqual(['amemory', 'bytes']); + await userEvent.click(screen.getByText('Empty fields')); + expect(getEmptyFieldsNames()).toEqual(['client', 'source', 'timestampLabel']); }); it('should show meta fields accordion', async () => { - const wrapper = await mountAndWaitForLazyModules( - - ); - - wrapper - .find('[data-test-subj="lnsIndexPatternMetaFields"]') - .find('button') - .first() - .simulate('click'); - expect( - wrapper - .find('[data-test-subj="lnsIndexPatternMetaFields"]') - .find(FieldItem) - .first() - .prop('field').name - ).toEqual('_id'); + }, + }), + }); + await userEvent.click(screen.getByText('Meta fields')); + expect(getMetaFieldsNames()).toEqual(['_id']); }); it('should display NoFieldsCallout when all fields are empty', async () => { - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - return { - existingFieldNames: [], - }; + loadFieldExistingMock.mockResolvedValueOnce({ + existingFieldNames: [], + indexPatternTitle: 'idx1', }); + await renderFormBasedDataPanel(); + expect(getAvailableFieldsNames()).toEqual([]); + expect( + screen.getByTestId('lnsIndexPatternAvailableFieldsNoFieldsCallout-noFieldsMatch') + ).toBeInTheDocument(); - const wrapper = await mountAndWaitForLazyModules(); + await userEvent.click(screen.getByText('Empty fields')); - expect(wrapper.find(EuiCallOut).length).toEqual(2); - expect( - wrapper - .find('[data-test-subj="lnsIndexPatternAvailableFields"]') - .find(FieldItem) - .map((fieldItem) => fieldItem.prop('field').name) - ).toEqual([]); - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"]') - .find('button') - .first() - .simulate('click'); - expect( - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"]') - .find(FieldItem) - .map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName) - ).toEqual(['amemory', 'bytes', 'client', 'source', 'timestampLabel']); + expect(getEmptyFieldsNames()).toEqual([ + 'amemory', + 'bytes', + 'client', + 'source', + 'timestampLabel', + ]); }); it('should display spinner for available fields accordion if existing fields are not loaded yet', async () => { - let resolveFunction: (arg: unknown) => void; - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockReset(); - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(() => { - return new Promise((resolve) => { + let resolveFunction: (arg: { + existingFieldNames: string[]; + indexPatternTitle: string; + }) => void; + loadFieldExistingMock.mockResolvedValueOnce( + new Promise((resolve) => { resolveFunction = resolve; - }); - }); - const wrapper = await mountAndWaitForLazyModules(); - + }) + ); + await renderFormBasedDataPanel(); + expect(screen.getByTestId('fieldListLoading')).toBeInTheDocument(); expect( - wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]').find(EuiLoadingSpinner) - .length - ).toEqual(1); - expect(wrapper.find(EuiCallOut).length).toEqual(0); + screen.queryByTestId('lnsIndexPatternMetaFieldsNoFieldsCallout-noFieldsMatch') + ).not.toBeInTheDocument(); await act(async () => { resolveFunction!({ existingFieldNames: [], + indexPatternTitle: 'idx1', }); }); - await act(async () => { - await wrapper.update(); - }); - + expect(screen.queryByTestId('fieldListLoading')).not.toBeInTheDocument(); expect( - wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]').find(EuiLoadingSpinner) - .length - ).toEqual(0); - expect(wrapper.find(EuiCallOut).length).toEqual(2); + screen.getByTestId('lnsIndexPatternMetaFieldsNoFieldsCallout-noFieldsMatch') + ).toBeInTheDocument(); }); - it('should not allow field details when error', async () => { - (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => { - throw new Error('test'); + it('should show error when loading fails', async () => { + loadFieldExistingMock.mockImplementation(() => { + throw new Error('idx1'); }); - const wrapper = await mountAndWaitForLazyModules(); - - expect(wrapper.find(FieldListGrouped).prop('fieldGroups')).toEqual( - expect.objectContaining({ - AvailableFields: expect.objectContaining({ hideDetails: true }), - }) - ); + await renderFormBasedDataPanel(); + expect(screen.getByTestId('lnsIndexPatternAvailableFields-fetchWarning')).toBeInTheDocument(); }); it('should filter down by name', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"] input').simulate('change', { - target: { value: 'me' }, - }); - }); - - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"] button') - .first() - .simulate('click'); - - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'memory', - 'timestamp', - ]); + await renderFormBasedDataPanel(); + await searchForPhrase('me'); + await userEvent.click(screen.getByText('Empty fields')); + expect(getFieldNames()).toEqual(['amemory', 'timestampLabel']); }); it('should announce filter in live region', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"] input').simulate('change', { - target: { value: 'me' }, - }); - }); - - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"]') - .find('button') - .first() - .simulate('click'); - - expect(wrapper.find('[aria-live="polite"]').at(1).text()).toEqual( - '1 available field. 1 empty field. 0 meta fields.' + await renderFormBasedDataPanel(); + await searchForPhrase('me'); + expect(getFieldNames()).toEqual(['amemory']); + expect(getAriaDescription()).toBe('1 available field. 1 empty field. 0 meta fields.'); + await userEvent.click(screen.getByTestId('lnsIndexPatternEmptyFields')); + + expect(screen.getByTestId('lnsIndexPattern__ariaDescription').getAttribute('aria-live')).toBe( + 'polite' ); }); it('should filter down by type', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - wrapper - .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') - .last() - .simulate('click'); + await renderFormBasedDataPanel(); - wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); + await userEvent.click(screen.getByTestId('lnsIndexPatternFieldTypeFilterToggle')); + await userEvent.click(screen.getByTestId('typeFilter-document')); - expect( - wrapper - .find(FieldItem) - .map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName) - ).toEqual(['amemory', 'bytes']); + expect(getFieldNames()).toEqual(['Records']); }); it('should display no fields in groups when filtered by type Record', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - wrapper - .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') - .last() - .simulate('click'); - - wrapper.find('[data-test-subj="typeFilter-document"]').first().simulate('click'); - - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - DOCUMENT_FIELD_NAME, - ]); - expect(wrapper.find(EuiCallOut).length).toEqual(2); + await renderFormBasedDataPanel(); + await userEvent.click(screen.getByTestId('lnsIndexPatternFieldTypeFilterToggle')); + await userEvent.click(screen.getByTestId('typeFilter-document')); + expect(screen.getAllByTestId('lnsFieldListPanelField').length).toEqual(1); + expect(getFieldNames()).toEqual(['Records']); + expect( + screen.getByTestId('lnsIndexPatternAvailableFieldsNoFieldsCallout-noFieldsMatch') + ).toBeInTheDocument(); + expect( + screen.getByTestId('lnsIndexPatternMetaFieldsNoFieldsCallout-noFieldsMatch') + ).toBeInTheDocument(); }); it('should toggle type if clicked again', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - wrapper - .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') - .last() - .simulate('click'); - - wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"]') - .find('button') - .first() - .simulate('click'); - expect( - wrapper - .find(FieldItem) - .map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName) - ).toEqual(['Records', 'amemory', 'bytes', 'client', 'source', 'timestampLabel']); + await renderFormBasedDataPanel(); + await userEvent.click(screen.getByTestId('lnsIndexPatternFieldTypeFilterToggle')); + await userEvent.click(screen.getByTestId('typeFilter-number')); + expect(getFieldNames()).toEqual(['amemory', 'bytes']); + await userEvent.click(screen.getByTestId('typeFilter-number')); + expect(getFieldNames()).toEqual(['Records', 'amemory', 'bytes']); }); - it('should filter down by type and by name', async () => { - const wrapper = await mountAndWaitForLazyModules(); - - act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"] input').simulate('change', { - target: { value: 'me' }, - }); - }); - - wrapper - .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') - .last() - .simulate('click'); - - wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'memory', - ]); + it('should filter down by names', async () => { + await renderFormBasedDataPanel(); + await searchForPhrase('me'); + expect(getFieldNames()).toEqual(['amemory']); + }); + it('should filter down by types', async () => { + await renderFormBasedDataPanel(); + await userEvent.click(screen.getByTestId('lnsIndexPatternFieldTypeFilterToggle')); + await userEvent.click(screen.getByTestId('typeFilter-number')); + expect(getFieldNames()).toEqual(['amemory', 'bytes']); }); }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx index c666c23d667c3..e356a59956f06 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx @@ -41,7 +41,7 @@ import type { FormBasedPrivateState } from './types'; import { IndexPatternServiceAPI } from '../../data_views_service/service'; import { FieldItem } from '../common/field_item'; -export type Props = Omit< +export type FormBasedDataPanelProps = Omit< DatasourceDataPanelProps, 'core' | 'onChangeIndexPattern' > & { @@ -97,7 +97,7 @@ export function FormBasedDataPanel({ onIndexPatternRefresh, usedIndexPatterns, layerFields, -}: Props) { +}: FormBasedDataPanelProps) { const { indexPatterns, indexPatternRefs } = frame.dataViews; const { currentIndexPatternId } = state; diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx index 4bd00983a212a..a354b54fb37e9 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx @@ -187,8 +187,7 @@ function mountWithServices(component: React.ReactElement): ReactWrapper { * - Dimension trigger: Not tested here * - Dimension editor component: First half of the tests */ -// Failing: See https://github.com/elastic/kibana/issues/192476 -describe.skip('FormBasedDimensionEditor', () => { +describe('FormBasedDimensionEditor', () => { let state: FormBasedPrivateState; let setState: jest.Mock; let defaultProps: FormBasedDimensionEditorProps; @@ -353,8 +352,7 @@ describe.skip('FormBasedDimensionEditor', () => { await userEvent.click(screen.getByRole('button', { name: /open list of options/i })); expect(screen.getByText(/There aren't any options available/)).toBeInTheDocument(); }); - - it('should list all field names and document as a whole in prioritized order', async () => { + test('should list all field names and document as a whole in prioritized order', async () => { const { getVisibleFieldSelectOptions } = renderDimensionPanel(); const comboBoxButton = screen.getAllByRole('button', { name: /open list of options/i })[0]; @@ -372,14 +370,9 @@ describe.skip('FormBasedDimensionEditor', () => { ]; expect(allOptions.slice(0, 7)).toEqual(getVisibleFieldSelectOptions()); - // keep hitting arrow down to scroll to the next options (react-window only renders visible options) - await userEvent.type(comboBoxInput, '{ArrowDown}'.repeat(12)); - - expect(getVisibleFieldSelectOptions()).toEqual(allOptions.slice(5, 16)); - - // press again to go back to the beginning - await userEvent.type(comboBoxInput, '{ArrowDown}'); - expect(getVisibleFieldSelectOptions()).toEqual(allOptions.slice(0, 9)); + // // press arrow up to go back to the beginning + await userEvent.type(comboBoxInput, '{ArrowUp}{ArrowUp}'); + expect(getVisibleFieldSelectOptions()).toEqual(allOptions.slice(8)); }); it('should hide fields that have no data', () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx index 322c1ab855152..619d02f0bf28f 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx @@ -7,9 +7,10 @@ import React from 'react'; import { mount } from 'enzyme'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { EuiComboBox } from '@elastic/eui'; +import userEvent from '@testing-library/user-event'; import { GenericOperationDefinition } from '../operations'; import { averageOperation, @@ -28,38 +29,8 @@ import { GenericIndexPatternColumn, FormBasedLayer, FormBasedPrivateState } from import { ReferenceBasedIndexPatternColumn } from '../operations/definitions/column_types'; import { FieldSelect } from './field_select'; import { IndexPattern, VisualizationDimensionGroupConfig } from '../../../types'; -import userEvent from '@testing-library/user-event'; - -jest.mock('../operations/layer_helpers', () => { - const original = jest.requireActual('../operations/layer_helpers'); - return { - ...original, - insertOrReplaceColumn: () => { - return {} as FormBasedLayer; - }, - }; -}); - -const defaultProps = { - indexPattern: createMockedIndexPattern(), - currentFieldIsInvalid: false, - incompleteField: null, - incompleteOperation: undefined, - incompleteParams: {}, - dimensionGroups: [] as VisualizationDimensionGroupConfig[], - groupId: 'any', - operationDefinitionMap: { - terms: termsOperation, - average: averageOperation, - count: countOperation, - differences: derivativeOperation, - staticValue: staticValueOperation, - min: minOperation, - } as unknown as Record, -}; - -function getStringBasedOperationColumn(field = 'source'): FieldBasedIndexPatternColumn { +function getStringBasedOperationColumn(field = 'source') { return { label: `Top value of ${field}`, dataType: 'string', @@ -109,12 +80,32 @@ function getCountOperationColumn(): GenericIndexPatternColumn { operationType: 'count', }; } -function getLayer( - col1: GenericIndexPatternColumn = getStringBasedOperationColumn(), - indexPattern?: IndexPattern -) { +const defaultDataView = createMockedIndexPattern(); +const defaultProps = { + columnId: 'col1', + layer: getLayer(), + operationSupportMatrix: getDefaultOperationSupportMatrix(getLayer(), 'col1'), + indexPattern: defaultDataView, + currentFieldIsInvalid: false, + incompleteField: null, + incompleteOperation: undefined, + incompleteParams: {}, + dimensionGroups: [] as VisualizationDimensionGroupConfig[], + groupId: 'any', + updateLayer: jest.fn(), + operationDefinitionMap: { + terms: termsOperation, + average: averageOperation, + count: countOperation, + differences: derivativeOperation, + staticValue: staticValueOperation, + min: minOperation, + } as unknown as Record, +}; + +function getLayer(col1: GenericIndexPatternColumn = getStringBasedOperationColumn()) { return { - indexPatternId: defaultProps.indexPattern.id, + indexPatternId: defaultDataView.id, columnOrder: ['col1', 'col2'], columns: { col1, @@ -135,53 +126,28 @@ function getDefaultOperationSupportMatrix( filterOperations: () => true, columnId, indexPatterns: { - [defaultProps.indexPattern.id]: indexPattern ?? defaultProps.indexPattern, + [defaultDataView.id]: indexPattern ?? defaultDataView, }, }); } -const mockedReader = { - hasFieldData: (dataViewId: string, fieldName: string) => { - if (defaultProps.indexPattern.id !== dataViewId) { - return false; - } - - const map: Record = {}; - for (const field of defaultProps.indexPattern.fields) { - map[field.name] = true; - } - - return map[fieldName]; - }, -}; - -jest.mock('@kbn/unified-field-list/src/hooks/use_existing_fields', () => ({ - useExistingFieldsReader: jest.fn(() => mockedReader), -})); - const renderFieldInput = ( overrideProps?: Partial> ) => { - const updateLayerSpy = jest.fn(); - const layer = getLayer(); - const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); - return render( - - ); + return render(); }; +const waitForComboboxToClose = async () => + await waitFor(() => expect(screen.queryByRole('listbox')).toBeNull()); + const getErrorElement = (container: HTMLElement) => container.querySelector('.euiFormErrorText'); const getLabelElement = () => screen.getByTestId('indexPattern-field-selection-row').querySelector('label'); describe('FieldInput', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('should render a field select box', () => { renderFieldInput(); expect(screen.getByTestId('indexPattern-dimension-field')).toBeInTheDocument(); @@ -248,6 +214,14 @@ describe('FieldInput', () => { ); }); + it('should update the layer on field selection', async () => { + renderFieldInput({ selectedColumn: getStringBasedOperationColumn() }); + await userEvent.click(screen.getByRole('combobox')); + fireEvent.click(screen.getByTestId('lns-fieldOption-bytes')); + expect(defaultProps.updateLayer).toHaveBeenCalled(); + await waitForComboboxToClose(); + }); + it('should prioritize errors over help messages', () => { const helpMessage = 'My help message'; renderFieldInput({ helpMessage, currentFieldIsInvalid: true }); @@ -256,32 +230,16 @@ describe('FieldInput', () => { ); }); - it('should update the layer on field selection', async () => { - const updateLayerSpy = jest.fn(); - renderFieldInput({ - selectedColumn: getStringBasedOperationColumn(), - updateLayer: updateLayerSpy, - }); - await userEvent.click(screen.getByRole('combobox')); - fireEvent.click(screen.getByTestId('lns-fieldOption-bytes')); - expect(updateLayerSpy).toHaveBeenCalled(); - }); - it('should not trigger when the same selected field is selected again', async () => { - const updateLayerSpy = jest.fn(); - renderFieldInput({ - selectedColumn: getStringBasedOperationColumn(), - updateLayer: updateLayerSpy, - }); - + renderFieldInput({ selectedColumn: getStringBasedOperationColumn() }); await userEvent.click(screen.getByRole('combobox')); fireEvent.click(screen.getByTestId('lns-fieldOption-source')); - expect(updateLayerSpy).not.toHaveBeenCalled(); + await waitForComboboxToClose(); + expect(defaultProps.updateLayer).not.toHaveBeenCalled(); }); it('should prioritize incomplete fields over selected column field to display', () => { - const updateLayerSpy = jest.fn(); const layer = getLayer(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); const instance = mount( @@ -289,7 +247,7 @@ describe('FieldInput', () => { {...defaultProps} layer={layer} columnId={'col1'} - updateLayer={updateLayerSpy} + updateLayer={defaultProps.updateLayer} operationSupportMatrix={operationSupportMatrix} incompleteField={'dest'} selectedColumn={getStringBasedOperationColumn()} @@ -305,7 +263,6 @@ describe('FieldInput', () => { }); it('should forward the onDeleteColumn function', () => { - const updateLayerSpy = jest.fn(); const onDeleteColumn = jest.fn(); const layer = getLayer(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); @@ -314,7 +271,7 @@ describe('FieldInput', () => { {...defaultProps} layer={layer} columnId={'col1'} - updateLayer={updateLayerSpy} + updateLayer={defaultProps.updateLayer} operationSupportMatrix={operationSupportMatrix} onDeleteColumn={onDeleteColumn} /> @@ -325,7 +282,7 @@ describe('FieldInput', () => { }); expect(onDeleteColumn).toHaveBeenCalled(); - expect(updateLayerSpy).not.toHaveBeenCalled(); + expect(defaultProps.updateLayer).not.toHaveBeenCalled(); }); describe('time series group', () => { @@ -341,7 +298,6 @@ describe('FieldInput', () => { return layer; } it('should not render the time dimension category if it has tsdb metric column but the group is not a breakdown', () => { - const updateLayerSpy = jest.fn(); const layer = getLayerWithTSDBMetric(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); const instance = mount( @@ -359,7 +315,7 @@ describe('FieldInput', () => { ])} layer={layer} columnId={'col1'} - updateLayer={updateLayerSpy} + updateLayer={defaultProps.updateLayer} operationSupportMatrix={operationSupportMatrix} /> ); @@ -368,7 +324,6 @@ describe('FieldInput', () => { }); it('should render the time dimension category if it has tsdb metric column and the group is a breakdown one', () => { - const updateLayerSpy = jest.fn(); const layer = getLayerWithTSDBMetric(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); const instance = mount( @@ -393,7 +348,7 @@ describe('FieldInput', () => { groupId="breakdown" layer={layer} columnId={'col1'} - updateLayer={updateLayerSpy} + updateLayer={defaultProps.updateLayer} operationSupportMatrix={operationSupportMatrix} /> ); diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx index 3e44070ad5152..c8875bfece64e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx @@ -63,7 +63,7 @@ const renderFormatSelector = (propsOverrides?: Partial) => // see for example the first two tests, they run the same code but expect // different results. With the updated userEvent code the tests no longer work // with this setup and should be refactored. -describe.skip('FormatSelector', () => { +describe('FormatSelector', () => { let user: UserEvent; beforeEach(() => { @@ -83,7 +83,7 @@ describe.skip('FormatSelector', () => { }); it('updates the format decimals to upper range when input exceeds the range', async () => { renderFormatSelector(); - await user.type(screen.getByLabelText('Decimals'), '{backspace}10'); + await user.type(screen.getByLabelText('Decimals'), '{backspace}20'); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { decimals: 15 } }); }); it('updates the format decimals to lower range when input is smaller than range', async () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/layerpanel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/layerpanel.test.tsx index a944513acf4e4..bbcecf15d2bd1 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/layerpanel.test.tsx @@ -13,6 +13,9 @@ import { getFieldByNameFactory } from './pure_helpers'; import { TermsIndexPatternColumn } from './operations'; import userEvent from '@testing-library/user-event'; +Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { value: 400 }); +Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 200 }); + jest.mock('@kbn/unified-search-plugin/public', () => { const actual = jest.requireActual('@kbn/unified-search-plugin/public'); return { @@ -219,9 +222,7 @@ describe('Layer Data Panel', () => { }; }); - const renderLayerPanel = () => { - return render(); - }; + const renderLayerPanel = () => render(); it('should list all index patterns', async () => { renderLayerPanel(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch/chart_switch.test.tsx index e8267dd388520..6795cd8caaa83 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch/chart_switch.test.tsx @@ -264,8 +264,8 @@ describe('chart_switch', () => { fireEvent.click(getMenuItem(subType)); }; - const waitForChartSwitchClosed = () => { - waitFor(() => { + const waitForChartSwitchClosed = async () => { + await waitFor(() => { expect(screen.queryByTestId('lnsChartSwitchList')).not.toBeInTheDocument(); }); }; @@ -448,14 +448,13 @@ describe('chart_switch', () => { await openChartSwitch(); switchToVis('testVis2'); - // expect(datasourceMap.testDatasource.publicAPIMock.getTableSpec).toHaveBeenCalled(); expect(visualizationMap.testVis2.getSuggestions).toHaveBeenCalled(); expect(visualizationMap.testVis2.initialize).toHaveBeenCalledWith( expect.any(Function), // generated layerId undefined, undefined ); - waitForChartSwitchClosed(); + await waitForChartSwitchClosed(); }); it('should use initial state if there is no suggestion from the target visualization', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 75cddd85b5803..7d41e326372e5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -444,52 +444,6 @@ describe('editor_frame', () => { instance.unmount(); }); - // this test doesn't test anything, it's buggy and should be rewritten when we find a way to user test drag and drop - it.skip('should switch to best suggested visualization on field drop', async () => { - const suggestionVisState = {}; - - visualizationMap = { - testVis: { - ...mockVisualization, - getSuggestions: () => [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], - }, - testVis2: mockVisualization2, - }; - datasourceMap = { - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - }, - }; - renderEditorFrame(); - - mockVisualization.getConfiguration.mockClear(); - act(() => { - instance.find('[data-test-subj="lnsWorkspace"]').last().simulate('drop'); - }); - - expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( - expect.objectContaining({ - state: {}, - }) - ); - }); - it('should use the currently selected visualization if possible on field drop', async () => { mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); const suggestionVisState = {}; diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index c4efd626d4772..e1de5e9b6527d 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -14,6 +14,9 @@ import { css } from '@emotion/react'; import { type IndexPatternRef } from '../../types'; import { type ChangeIndexPatternTriggerProps, TriggerButton } from './trigger'; +const MAX_WIDTH = 600; +const MIN_WIDTH = 320; + export function ChangeIndexPattern({ indexPatternRefs, isMissingCurrent, @@ -52,8 +55,8 @@ export function ChangeIndexPattern({
diff --git a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx index 98a040ab04bb5..8538dcece585e 100644 --- a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx @@ -142,12 +142,14 @@ describe('Legend Settings', () => { toolTipContent: 'Shows the average value', }, ], + legendStats: [LegendValue.Average], onLegendStatsChange, }); - expect(screen.queryByRole('button', { name: 'Layout' })).toBeNull(); fireEvent.click(screen.getByRole('combobox', { name: 'Statistics' })); fireEvent.click(screen.getByRole('option', { name: 'Current and last value' })); - // expect(screen.getByRole('group', { name: 'Layout' })).toBeInTheDocument(); - expect(onLegendStatsChange).toBeCalledWith([LegendValue.CurrentAndLastValue], false); + expect(onLegendStatsChange).toBeCalledWith( + [LegendValue.Average, LegendValue.CurrentAndLastValue], + false + ); }); }); diff --git a/x-pack/plugins/lens/public/visualization_container.test.tsx b/x-pack/plugins/lens/public/visualization_container.test.tsx index 9ffaebe1f14e9..498a44c2760f7 100644 --- a/x-pack/plugins/lens/public/visualization_container.test.tsx +++ b/x-pack/plugins/lens/public/visualization_container.test.tsx @@ -6,31 +6,25 @@ */ import React from 'react'; -import { mount } from 'enzyme'; import { VisualizationContainer } from './visualization_container'; +import { render, screen } from '@testing-library/react'; describe('VisualizationContainer', () => { + const renderVisContainer = (props?: React.HTMLAttributes) => { + return render(Hello!); + }; test('renders child content', () => { - const component = mount(Hello!); - - expect(component.text()).toEqual('Hello!'); + renderVisContainer(); + expect(screen.getByText('Hello!')).toBeInTheDocument(); }); test('renders style', () => { - const component = mount( - Hello! - ); - const el = component.find('.lnsVisualizationContainer').first(); - - expect(el.prop('style')).toEqual({ color: 'blue' }); + renderVisContainer({ style: { color: 'blue' } }); + expect(screen.getByText('Hello!')).toHaveStyle({ color: 'blue' }); }); test('combines class names with container class', () => { - const component = mount( - Hello! - ); - const el = component.find('.lnsVisualizationContainer').first(); - - expect(el.prop('className')).toEqual('myClass lnsVisualizationContainer'); + renderVisContainer({ className: 'myClass' }); + expect(screen.getByText('Hello!')).toHaveClass('myClass lnsVisualizationContainer'); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx index 8cf548255023a..09c7d95b309e7 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx @@ -24,6 +24,7 @@ import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks'; import { TableDimensionEditor } from './dimension_editor'; import { ColumnState } from '../../../../common/expressions'; import { capitalize } from 'lodash'; +import { I18nProvider } from '@kbn/i18n-react'; describe('data table dimension editor', () => { let user: UserEvent; @@ -60,7 +61,6 @@ describe('data table dimension editor', () => { }); beforeEach(() => { - // Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841 user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); btnGroups = { colorMode: new EuiButtonGroupTestHarness('lnsDatatable_dynamicColoring_groups'), @@ -123,10 +123,10 @@ describe('data table dimension editor', () => { ) => { return render(, { wrapper: ({ children }) => ( - <> +
{children} - + ), }); }; @@ -243,6 +243,7 @@ describe('data table dimension editor', () => { renderTableDimensionEditor(); await user.click(screen.getByLabelText('Edit colors')); + act(() => jest.advanceTimersByTime(256)); expect(screen.getByTestId(`lns-palettePanel-${flyout}`)).toBeInTheDocument(); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index 2ac9b3df7fdd2..694799b94638e 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -193,6 +193,18 @@ const areaShared = { }), }; +const lineShared = { + id: 'line', + icon: IconChartLine, + label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { + defaultMessage: 'Line', + }), + sortPriority: 2, + description: i18n.translate('xpack.lens.line.visualizationDescription', { + defaultMessage: 'Reveal variations in data over time.', + }), +}; + export const visualizationSubtypes: VisualizationType[] = [ { id: 'bar', @@ -278,17 +290,7 @@ export const visualizationSubtypes: VisualizationType[] = [ }), ...areaShared, }, - { - id: 'line', - icon: IconChartLine, - label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { - defaultMessage: 'Line', - }), - sortPriority: 2, - description: i18n.translate('xpack.lens.line.visualizationDescription', { - defaultMessage: 'Reveal variations in data over time.', - }), - }, + lineShared, ]; export const visualizationTypes: VisualizationType[] = [ @@ -306,10 +308,7 @@ export const visualizationTypes: VisualizationType[] = [ label: i18n.translate('xpack.lens.xyVisualization.barLabel', { defaultMessage: 'Bar', }), - sortPriority: 1, - description: i18n.translate('xpack.lens.bar.visualizationDescription', { - defaultMessage: 'Compare categories or groups of data via bars.', - }), + ...barShared, getCompatibleSubtype: (seriesType?: string) => { if (seriesType === 'area') { return 'bar'; @@ -342,15 +341,7 @@ export const visualizationTypes: VisualizationType[] = [ }, }, { - id: 'line', - icon: IconChartLine, - label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { - defaultMessage: 'Line', - }), - sortPriority: 2, - description: i18n.translate('xpack.lens.line.visualizationDescription', { - defaultMessage: 'Reveal variations in data over time or categorically.', - }), + ...lineShared, subtypes: ['line'], }, ];