diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 5e2fe9d7bbc14b..a62db353e2bafa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -57,7 +57,7 @@ export function ChangeIndexPattern({ panelPaddingSize="s" ownFocus > -
+
{i18n.translate('xpack.lens.indexPattern.changeIndexPatternTitle', { defaultMessage: 'Change index pattern', diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx index 369ab28293fbc8..5a68516db6aa3f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx @@ -122,6 +122,7 @@ export const pieVisualization: Visualization { ); const options = component - .find('[data-test-subj="lnsXY_seriesType"]') + .find(EuiButtonGroup) .first() .prop('options') as EuiButtonGroupProps['options']; @@ -79,7 +79,7 @@ describe('XY Config panels', () => { ); const options = component - .find('[data-test-subj="lnsXY_seriesType"]') + .find(EuiButtonGroup) .first() .prop('options') as EuiButtonGroupProps['options']; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 6d5bc7808a678f..e4bc6de5cc68ae 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -95,13 +95,13 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { })} name="chartType" className="eui-displayInlineBlock" - data-test-subj="lnsXY_seriesType" options={visualizationTypes .filter((t) => isHorizontalSeries(t.id as SeriesType) === horizontalOnly) .map((t) => ({ id: t.id, label: t.label, iconType: t.icon || 'empty', + 'data-test-subj': `lnsXY_seriesType-${t.id}`, }))} idSelected={layer.seriesType} onChange={(seriesType) => { diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 2c05b02cf470f5..0ae4753cd2967d 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -191,8 +191,9 @@ export default ({ getService }: FtrProviderContext) => { expect(results.saved_overall).to.eql({ lnsMetric: 1, bar_stacked: 1, + lnsPie: 1, }); - expect(results.saved_overall_total).to.eql(2); + expect(results.saved_overall_total).to.eql(3); await esArchiver.unload('lens/basic'); }); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts new file mode 100644 index 00000000000000..ccf2f88a9d0ed1 --- /dev/null +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['header', 'common', 'dashboard', 'timePicker', 'lens']); + + const find = getService('find'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const elasticChart = getService('elasticChart'); + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + + async function clickInChart(x: number, y: number) { + const el = await elasticChart.getCanvas(); + await browser.getActions().move({ x, y, origin: el._webElement }).click().perform(); + } + + describe('lens dashboard tests', () => { + it('metric should be embeddable', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Artistpreviouslyknownaslens'); + await find.clickByButtonText('Artistpreviouslyknownaslens'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + }); + + it('should be able to add filters/timerange by clicking in XYChart', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + await clickInChart(5, 5); // hardcoded position of bar + + await retry.try(async () => { + await testSubjects.click('applyFiltersPopoverButton'); + await testSubjects.missingOrFail('applyFiltersPopoverButton'); + }); + + await PageObjects.lens.assertExactText( + '[data-test-subj="embeddablePanelHeading-lnsXYvis"]', + 'lnsXYvis' + ); + const time = await PageObjects.timePicker.getTimeConfig(); + expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000'); + expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000'); + const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); + expect(hasIpFilter).to.be(true); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index b17b7d856841c9..f2dcf28c017433 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -28,6 +28,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags(['ciGroup4', 'skipFirefox']); loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./persistent_context')); loadTestFile(require.resolve('./lens_reporting')); }); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 1e936361610678..77b9aa1e25edd8 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -8,115 +8,20 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects([ - 'header', - 'common', - 'visualize', - 'dashboard', - 'header', - 'timePicker', - 'lens', - ]); + const PageObjects = getPageObjects(['visualize', 'lens']); const find = getService('find'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const elasticChart = getService('elasticChart'); - const browser = getService('browser'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const filterBar = getService('filterBar'); const listingTable = getService('listingTable'); - async function assertExpectedMetric(metricCount: string = '19,986') { - await PageObjects.lens.assertExactText( - '[data-test-subj="lns_metric_title"]', - 'Maximum of bytes' - ); - await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', metricCount); - } - - async function assertExpectedTable() { - await PageObjects.lens.assertExactText( - '[data-test-subj="lnsDataTable"] thead .euiTableCellContent__text', - 'Maximum of bytes' - ); - await PageObjects.lens.assertExactText( - '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValue"]', - '19,986' - ); - } - - async function assertExpectedChart() { - await PageObjects.lens.assertExactText( - '[data-test-subj="embeddablePanelHeading-lnsXYvis"]', - 'lnsXYvis' - ); - } - - async function assertExpectedTimerange() { - const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000'); - expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000'); - } - - async function clickOnBarHistogram() { - const el = await elasticChart.getCanvas(); - await browser.getActions().move({ x: 5, y: 5, origin: el._webElement }).click().perform(); - } - describe('lens smokescreen tests', () => { it('should allow editing saved visualizations', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); await PageObjects.lens.goToTimeRange(); - await assertExpectedMetric(); - }); - - it('metric should be embeddable in dashboards', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.filterEmbeddableNames('Artistpreviouslyknownaslens'); - await find.clickByButtonText('Artistpreviouslyknownaslens'); - await dashboardAddPanel.closeAddPanel(); - await PageObjects.lens.goToTimeRange(); - await assertExpectedMetric(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }); - it('click on the bar in XYChart adds proper filters/timerange in dashboard', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); - await find.clickByButtonText('lnsXYvis'); - await dashboardAddPanel.closeAddPanel(); - await PageObjects.lens.goToTimeRange(); - await clickOnBarHistogram(); - - await retry.try(async () => { - await testSubjects.click('applyFiltersPopoverButton'); - await testSubjects.missingOrFail('applyFiltersPopoverButton'); - }); - - await assertExpectedChart(); - await assertExpectedTimerange(); - const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); - expect(hasIpFilter).to.be(true); - }); - - it('should allow seamless transition to and from table view', async () => { - await PageObjects.visualize.gotoVisualizationLandingPage(); - await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); - await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); - await PageObjects.lens.goToTimeRange(); - await assertExpectedMetric(); - await PageObjects.lens.switchToVisualization('lnsDatatable'); - await assertExpectedTable(); - await PageObjects.lens.switchToVisualization('lnsMetric'); - await assertExpectedMetric(); - }); - - it('should allow creation of lens visualizations', async () => { + it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -165,6 +70,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + it('should allow seamless transition to and from table view', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('Maximum of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('19,986'); + await PageObjects.lens.switchToVisualization('lnsMetric'); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + }); + it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -190,5 +108,94 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getLayerCount()).to.eql(2); }); + + it('should allow transition from line chart to donut chart and to bar chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + expect(await PageObjects.lens.hasChartSwitchWarning('donut')).to.eql(true); + await PageObjects.lens.switchToVisualization('donut'); + + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql( + 'Top values of ip' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + + expect(await PageObjects.lens.hasChartSwitchWarning('bar')).to.eql(false); + await PageObjects.lens.switchToVisualization('bar'); + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + 'Top values of ip' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + + it('should allow seamless transition from bar chart to line chart using layer chart switch', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchLayerSeriesType('line'); + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Average of bytes' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_splitDimensionPanel')).to.eql( + 'Top values of ip' + ); + }); + + it('should allow seamless transition from pie chart to treemap chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsPieVis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsPieVis'); + await PageObjects.lens.goToTimeRange(); + expect(await PageObjects.lens.hasChartSwitchWarning('treemap')).to.eql(false); + await PageObjects.lens.switchToVisualization('treemap'); + expect( + await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 0) + ).to.eql('Top values of geo.dest'); + expect( + await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 1) + ).to.eql('Top values of geo.src'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + + it('should allow creating a pie chart and switching to datatable', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('pie'); + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('@timestamp per 3 hours'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2015-09-20 00:00'); + expect(await PageObjects.lens.getDatatableHeaderText(1)).to.eql('Average of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('6,011.351'); + }); }); } diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json.gz b/x-pack/test/functional/es_archives/lens/basic/data.json.gz index 4ed7c29f7391e4..ddf4a27289dffd 100644 Binary files a/x-pack/test/functional/es_archives/lens/basic/data.json.gz and b/x-pack/test/functional/es_archives/lens/basic/data.json.gz differ diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 79548db0e26301..bed0e3a159e23f 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -176,9 +176,26 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont */ async hasChartSwitchWarning(subVisualizationId: string) { await this.openChartSwitchPopover(); - const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`); - return await testSubjects.descendantExists('euiKeyPadMenuItem__betaBadgeWrapper', element); + return await find.descendantExistsByCssSelector( + '.euiKeyPadMenuItem__betaBadgeWrapper', + element + ); + }, + + /** + * Uses the Lens layer switcher to switch seriesType for xy charts. + * + * @param subVisualizationId - the ID of the sub-visualization to switch to, such as + * line, + */ + async switchLayerSeriesType(seriesType: string) { + await retry.try(async () => { + await testSubjects.click('lns_layer_settings'); + await testSubjects.exists(`lnsXY_seriesType-${seriesType}`); + }); + + return await testSubjects.click(`lnsXY_seriesType-${seriesType}`); }, /** @@ -205,5 +222,60 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('lnsApp_saveAndReturnButton'); }, + /** + * Gets label of dimension trigger in dimension panel + * + * @param dimension - the selector of the dimension + */ + async getDimensionTriggerText(dimension: string, index = 0) { + const dimensionElements = await testSubjects.findAll(dimension); + const trigger = await testSubjects.findDescendant( + 'lns-dimensionTrigger', + dimensionElements[index] + ); + return await trigger.getVisibleText(); + }, + + /** + * Gets text of the specified datatable header cell + * + * @param index - index of th element in datatable + */ + async getDatatableHeaderText(index = 0) { + return find + .byCssSelector( + `[data-test-subj="lnsDataTable"] thead th:nth-child(${ + index + 1 + }) .euiTableCellContent__text` + ) + .then((el) => el.getVisibleText()); + }, + + /** + * Gets text of the specified datatable cell + * + * @param rowIndex - index of row of the cell + * @param colIndex - index of column of the cell + */ + async getDatatableCellText(rowIndex = 0, colIndex = 0) { + return find + .byCssSelector( + `[data-test-subj="lnsDataTable"] tr:nth-child(${rowIndex + 1}) td:nth-child(${ + colIndex + 1 + })` + ) + .then((el) => el.getVisibleText()); + }, + + /** + * Asserts that metric has expected title and count + * + * @param title - expected title + * @param count - expected count of metric + */ + async assertMetric(title: string, count: string) { + await this.assertExactText('[data-test-subj="lns_metric_title"]', title); + await this.assertExactText('[data-test-subj="lns_metric_value"]', count); + }, }); }