diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index c70f1df820252..07d35b78b58a2 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -75,7 +75,7 @@ export interface FieldStatsProps { 'data-test-subj'?: string; overrideMissingContent?: (params: { element: JSX.Element; - noDataFound?: boolean; + reason: 'no-data' | 'unsupported'; }) => JSX.Element | null; overrideFooter?: (params: { element: JSX.Element; @@ -304,7 +304,7 @@ const FieldStatsComponent: React.FC = ({ return overrideMissingContent ? overrideMissingContent({ - noDataFound: false, + reason: 'unsupported', element: messageNoAnalysis, }) : messageNoAnalysis; @@ -338,7 +338,7 @@ const FieldStatsComponent: React.FC = ({ return overrideMissingContent ? overrideMissingContent({ - noDataFound: true, + reason: 'no-data', element: messageNoData, }) : messageNoData; @@ -358,12 +358,14 @@ const FieldStatsComponent: React.FC = ({ defaultMessage: 'Top values', }), id: 'topValues', + 'data-test-subj': `${dataTestSubject}-buttonGroup-topValuesButton`, }, { label: i18n.translate('unifiedFieldList.fieldStats.fieldDistributionLabel', { defaultMessage: 'Distribution', }), id: 'histogram', + 'data-test-subj': `${dataTestSubject}-buttonGroup-distributionButton`, }, ]} onChange={(optionId: string) => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx index 5ebfecc4cc95a..e2ee0559b3808 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx @@ -190,6 +190,9 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { initialFocus=".lnsFieldItem__fieldPanel" className="lnsFieldItem__popoverAnchor" data-test-subj="lnsFieldListPanelField" + panelProps={{ + 'data-test-subj': 'lnsFieldListPanelFieldContent', + }} container={document.querySelector('.application') || undefined} button={ { - if (params?.noDataFound) { + if (params.reason === 'no-data') { // TODO: should we replace this with a default message "Analysis is not available for this field?" const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling'); return ( - <> - - {isUsingSampling - ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', { - defaultMessage: - 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.', - }) - : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: - 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.', - })} - - + + {isUsingSampling + ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', { + defaultMessage: + 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.', + }) + : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { + defaultMessage: + 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.', + })} + + ); + } + if (params.reason === 'unsupported') { + return ( + + {params.element} + ); } - return params.element; }} /> diff --git a/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx b/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx index 105c9583e300d..d6b4c73b51082 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx @@ -169,14 +169,18 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ } if (hasLoaded) { return ( - + {fieldsCount} ); } return ; - }, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, fieldsCount]); + }, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, id, fieldsCount]); return ( { if (hasLoaded) { return ( - + {fields.length} ); } return ; - }, [fields.length, hasLoaded, isFiltered]); + }, [fields.length, hasLoaded, id, isFiltered]); return ( <> diff --git a/x-pack/test/functional/apps/lens/group1/fields_list.ts b/x-pack/test/functional/apps/lens/group1/fields_list.ts new file mode 100644 index 0000000000000..3d571483bf9ac --- /dev/null +++ b/x-pack/test/functional/apps/lens/group1/fields_list.ts @@ -0,0 +1,233 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const find = getService('find'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + const fieldEditor = getService('fieldEditor'); + const retry = getService('retry'); + + describe('lens fields list tests', () => { + for (const datasourceType of ['form-based', 'ad-hoc', 'ad-hoc-no-timefield']) { + describe(`${datasourceType} datasource`, () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + + if (datasourceType !== 'form-based') { + await PageObjects.lens.createAdHocDataView( + '*stash*', + datasourceType !== 'ad-hoc-no-timefield' + ); + retry.try(async () => { + const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern(); + expect(selectedPattern).to.eql('*stash*'); + }); + } + + if (datasourceType !== 'ad-hoc-no-timefield') { + await PageObjects.lens.goToTimeRange(); + } + + await retry.try(async () => { + await PageObjects.lens.clickAddField(); + await fieldEditor.setName('runtime_string'); + await fieldEditor.enableValue(); + await fieldEditor.typeScript("emit('abc')"); + await fieldEditor.save(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + }); + + it('should show all fields as available', async () => { + expect( + await (await testSubjects.find('lnsIndexPatternAvailableFields-count')).getVisibleText() + ).to.eql(53); + }); + + it('should show a histogram and top values popover for numeric field', async () => { + const [fieldId] = await PageObjects.lens.findFieldIdsByType('number'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + // check for popover + await testSubjects.exists('lnsFieldListPanel-title'); + // check for top values chart + await testSubjects.existOrFail('lnsFieldListPanel-topValues'); + const topValuesRows = await testSubjects.findAll('lnsFieldListPanel-topValues-bucket'); + expect(topValuesRows.length).to.eql(11); + // check for the Other entry + expect(await topValuesRows[10].getVisibleText()).to.eql('Other\n96.7%'); + // switch to date histogram + await testSubjects.click('lnsFieldListPanel-buttonGroup-distributionButton'); + // check for date histogram chart + expect( + await find.existsByCssSelector( + '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart' + ) + ).to.eql(true); + }); + + it('should show a top values popover for a keyword field', async () => { + const [fieldId] = await PageObjects.lens.findFieldIdsByType('string'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + // check for popover + await testSubjects.exists('lnsFieldListPanel-title'); + // check for top values chart + await testSubjects.existOrFail('lnsFieldListPanel-topValues'); + const topValuesRows = await testSubjects.findAll('lnsFieldListPanel-topValues-bucket'); + expect(topValuesRows.length).to.eql(11); + // check for the Other entry + expect(await topValuesRows[10].getVisibleText()).to.eql('Other\n99.9%'); + // check no date histogram + expect( + await find.existsByCssSelector( + '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart' + ) + ).to.eql(false); + }); + + it('should show a date histogram popover for a date field', async () => { + const [fieldId] = await PageObjects.lens.findFieldIdsByType('date'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + // check for popover + await testSubjects.exists('lnsFieldListPanel-title'); + // check for date histogram chart + expect( + await find.existsByCssSelector( + '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart' + ) + ).to.eql(true); + // check no top values chart + await testSubjects.missingOrFail('lnsFieldListPanel-buttonGroup-topValuesButton'); + }); + + it('should show a placeholder message about geo points field', async () => { + const [fieldId] = await PageObjects.lens.findFieldIdsByType('geo_point'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + const message = await testSubjects.getVisibleText('lnsFieldListPanel-missingFieldStats'); + expect(message).to.eql('Analysis is not available for this field.'); + }); + + it('should show stats for a numeric runtime field', async () => { + await PageObjects.lens.searchField('runtime'); + await PageObjects.lens.waitForField('runtime_number'); + const [fieldId] = await PageObjects.lens.findFieldIdsByType('number'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + // check for popover + await testSubjects.exists('lnsFieldListPanel-title'); + // check for top values chart + await testSubjects.existOrFail('lnsFieldListPanel-topValues'); + // check values + const topValuesRows = await testSubjects.findAll('lnsFieldListPanel-topValues-bucket'); + expect(topValuesRows.length).to.eql(11); + // check for the Other entry + expect(await topValuesRows[10].getVisibleText()).to.eql('Other\n96.7%'); + // switch to date histogram + await testSubjects.click('lnsFieldListPanel-buttonGroup-distributionButton'); + // check for date histogram chart + expect( + await find.existsByCssSelector( + '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart' + ) + ).to.eql(true); + }); + + it('should show stats for a keyword runtime field', async () => { + await PageObjects.lens.searchField('runtime'); + await PageObjects.lens.waitForField('runtime_string'); + const [fieldId] = await PageObjects.lens.findFieldIdsByType('string'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + // check for popover + await testSubjects.exists('lnsFieldListPanel-title'); + // check for top values chart + await testSubjects.existOrFail('lnsFieldListPanel-topValues'); + // check no date histogram + expect( + await find.existsByCssSelector( + '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart' + ) + ).to.eql(false); + await PageObjects.lens.searchField(''); + }); + + it('should change popover content if user defines a filter that affects field values', async () => { + // check the current records count for stats + const [fieldId] = await PageObjects.lens.findFieldIdsByType('string'); + await log.debug(`Opening field stats for ${fieldId}`); + await testSubjects.click(fieldId); + const valuesCount = parseInt( + (await testSubjects.getVisibleText('lnsFieldListPanel-statsFooter')) + .replaceAll(/(Calculated from | records\.)/g, '') + .replace(',', ''), + 10 + ); + // define a filter + await filterBar.addFilter('geo.src', 'is', 'CN'); + await retry.waitFor('Wait for the filter to take effect', async () => { + await testSubjects.click(fieldId); + // check for top values chart has changed compared to the previous test + const newValuesCount = parseInt( + (await testSubjects.getVisibleText('lnsFieldListPanel-statsFooter')) + .replaceAll(/(Calculated from | records\.)/g, '') + .replace(',', ''), + 10 + ); + return newValuesCount < valuesCount; + }); + }); + + // One Fields cap's limitation is to not know when an index has no fields based on filters + it('should detect fields have no data in popup if filter excludes them', async () => { + await filterBar.removeAllFilters(); + await filterBar.addFilter('bytes', 'is', '-1'); + // check via popup fields have no data + const [fieldId] = await PageObjects.lens.findFieldIdsByType('string'); + await log.debug(`Opening field stats for ${fieldId}`); + await retry.try(async () => { + await testSubjects.click(fieldId); + expect(await testSubjects.find('lnsFieldListPanel-missingFieldStats')).to.be.ok(); + // close the popover + await testSubjects.click(fieldId); + }); + }); + + if (datasourceType !== 'ad-hoc-no-timefield') { + it('should move some fields as empty when the time range excludes them', async () => { + // remove the filter + await filterBar.removeAllFilters(); + // tweak the time range to 17 Sept 2015 to 18 Sept 2015 + await PageObjects.lens.goToTimeRange( + 'Sep 17, 2015 @ 06:31:44.000', + 'Sep 18, 2015 @ 06:31:44.000' + ); + // check all fields are empty now + expect( + await (await testSubjects.find('lnsIndexPatternEmptyFields-count')).getVisibleText() + ).to.eql(52); + // check avaialble count is 0 + expect( + await ( + await testSubjects.find('lnsIndexPatternAvailableFields-count') + ).getVisibleText() + ).to.eql(1); + }); + } + }); + } + }); +} diff --git a/x-pack/test/functional/apps/lens/group1/index.ts b/x-pack/test/functional/apps/lens/group1/index.ts index 47f08a59e7341..302289319adbf 100644 --- a/x-pack/test/functional/apps/lens/group1/index.ts +++ b/x-pack/test/functional/apps/lens/group1/index.ts @@ -79,6 +79,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext loadTestFile(require.resolve('./table_dashboard')); loadTestFile(require.resolve('./table')); loadTestFile(require.resolve('./text_based_languages')); + loadTestFile(require.resolve('./fields_list')); loadTestFile(require.resolve('./layer_actions')); } }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 8dd95aa107929..c814b5b161fcd 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -1335,9 +1335,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('indexPattern-add-field'); }, - async createAdHocDataView(name: string) { + async createAdHocDataView(name: string, hasTimeField?: boolean) { await testSubjects.click('lns-dataView-switch-link'); - await PageObjects.unifiedSearch.createNewDataView(name, true); + await PageObjects.unifiedSearch.createNewDataView(name, true, hasTimeField); }, async switchToTextBasedLanguage(language: string) { @@ -1638,5 +1638,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }) ); }, + + async findFieldIdsByType( + type: 'string' | 'number' | 'date' | 'geo_point' | 'ip_range', + group: 'available' | 'empty' | 'meta' = 'available' + ) { + const groupCapitalized = `${group[0].toUpperCase()}${group.slice(1).toLowerCase()}`; + const allFieldsForType = await find.allByCssSelector( + `[data-test-subj="lnsIndexPattern${groupCapitalized}Fields"] .lnsFieldItem--${type}` + ); + // map to testSubjId + return Promise.all(allFieldsForType.map((el) => el.getAttribute('data-test-subj'))); + }, }); }