diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index a832b01277e2d..10d04031d0733 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -114,7 +114,17 @@ export const getShareAppMenuItem = ({ }, sharingData: { isTextBased: isEsqlMode, - locatorParams: [{ id: locator.id, params }], + locatorParams: [ + { + id: locator.id, + params: isEsqlMode + ? { + ...params, + timeRange: timefilter.getAbsoluteTime(), // Will be used when generating CSV on server. See `filtersFromLocator`. + } + : params, + }, + ], ...searchSourceSharingData, // CSV reports can be generated without a saved search so we provide a fallback title title: diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap index 1577d942ccab7..5e047707a1776 100644 --- a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -916,6 +916,25 @@ exports[`discover Discover CSV Export Generate CSV: new search generate a report " `; +exports[`discover Discover CSV Export Generate CSV: new search generate a report using ES|QL for relative time range as absolute dates and time params 1`] = ` +"name,numberValue +\\"test-487\\",486 +\\"test-488\\",487 +\\"test-489\\",488 +\\"test-490\\",489 +\\"test-491\\",490 +\\"test-492\\",491 +\\"test-493\\",492 +\\"test-494\\",493 +\\"test-495\\",494 +\\"test-496\\",495 +\\"test-497\\",496 +\\"test-498\\",497 +\\"test-499\\",498 +\\"test-500\\",499 +" +`; + exports[`discover Discover CSV Export Generate CSV: new search generates a large export 1`] = ` "\\"_id\\",\\"_ignored\\",\\"_index\\",\\"_score\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user 3AMtOW0BH63Xcmy432DJ,\\"-\\",ecommerce,\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,\\"(empty)\\",Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"POINT (54.4 24.5)\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 177f3c92e94e1..bb71966f5da64 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import moment from 'moment'; +import moment, { DurationInputArg2 } from 'moment'; import { Key } from 'selenium-webdriver'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -31,6 +31,72 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + const deleteIndex = async (index: string) => { + try { + await es.indices.delete({ index }); + } catch (err) { + // ignore 404 error + } + }; + + const createDocs = async ({ + index, + endDate, + docCount, + dateSubstractUnit, + addNumberField, + }: { + index: string; + endDate: string; + docCount: number; + dateSubstractUnit?: DurationInputArg2; + addNumberField?: boolean; + }) => { + interface TestDoc { + timestamp: string; + name: string; + updated_at?: string; + numberValue?: number; + } + + const docs = Array(docCount); + + for (let i = 0; i <= docs.length - 1; i++) { + const name = `test-${i + 1}`; + const timestamp = moment + .utc(endDate) + .subtract(docCount - i, dateSubstractUnit ?? 'days') + .format(); + + const commonFields: Pick = { + timestamp, + name, + }; + + if (addNumberField) { + commonFields.numberValue = i; + } + + if (i === 0) { + // only the oldest document has a value for updated_at + docs[i] = { + ...commonFields, + updated_at: moment.utc(endDate).format(), + }; + } else { + // updated_at field does not exist in first 500 documents + docs[i] = commonFields; + } + } + + const res = await es.bulk({ + index, + operations: docs.map((d) => `{"index": {}}\n${JSON.stringify(d)}\n`), + }); + + log.info(`Indexed ${res.items.length} test data docs into ${index}.`); + }; + const getReport = async ({ timeout } = { timeout: 60 * 1000 }) => { // close any open notification toasts await toasts.dismissAll(); @@ -46,6 +112,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return res; }; + const getReportPostUrl = async () => { + // click 'Copy POST URL' + await share.clickShareTopNavButton(); + await reporting.openExportTab(); + const copyButton = await testSubjects.find('shareReportingCopyURL'); + + return decodeURIComponent((await copyButton.getAttribute('data-share-url')) ?? ''); + }; + describe('Discover CSV Export', () => { describe('Check Available', () => { before(async () => { @@ -189,60 +264,61 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const csvFile = res.text; expectSnapshot(csvFile).toMatch(); }); + + it('generate a report using ES|QL for relative time range as absolute dates and time params', async () => { + const RECENT_DATA_INDEX_NAME = 'test_recent_data'; + const RECENT_DOC_COUNT = 500; + const RECENT_DOC_END_DATE = moment().toISOString(); + + await deleteIndex(RECENT_DATA_INDEX_NAME); + await createDocs({ + index: RECENT_DATA_INDEX_NAME, + endDate: RECENT_DOC_END_DATE, + docCount: RECENT_DOC_COUNT, + dateSubstractUnit: 'minutes', + addNumberField: true, + }); + + await timePicker.setCommonlyUsedTime('Last_15 minutes'); + await discover.selectTextBaseLang(); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const testQuery = `from ${RECENT_DATA_INDEX_NAME} | sort timestamp | WHERE timestamp >= ?_tstart AND timestamp <= ?_tend | KEEP name, numberValue`; + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const reportPostUrl = await getReportPostUrl(); + expect(reportPostUrl).to.contain(`timeRange:(from:'2`); // not `from:now-15m` + expect(reportPostUrl).to.contain(`filters:!()`); + expect(reportPostUrl).to.contain(`query:(esql:'${testQuery}')`); + + const res = await getReport(); + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + + const csvFile = res.text; + expectSnapshot(csvFile).toMatch(); + + await deleteIndex(RECENT_DATA_INDEX_NAME); + }); }); describe('Generate CSV: sparse data', () => { const TEST_INDEX_NAME = 'sparse_data'; const TEST_DOC_COUNT = 510; + const TEST_DOC_END_DATE = '2006-08-14T00:00:00'; - const reset = async () => { - try { - await es.indices.delete({ index: TEST_INDEX_NAME }); - } catch (err) { - // ignore 404 error - } - }; - - const createDocs = async () => { - interface TestDoc { - timestamp: string; - name: string; - updated_at?: string; - } - - const docs = Array(TEST_DOC_COUNT); - - for (let i = 0; i <= docs.length - 1; i++) { - const name = `test-${i + 1}`; - const timestamp = moment - .utc('2006-08-14T00:00:00') - .subtract(TEST_DOC_COUNT - i, 'days') - .format(); - - if (i === 0) { - // only the oldest document has a value for updated_at - docs[i] = { - timestamp, - name, - updated_at: moment.utc('2006-08-14T00:00:00').format(), - }; - } else { - // updated_at field does not exist in first 500 documents - docs[i] = { timestamp, name }; - } - } - - const res = await es.bulk({ + before(async () => { + await deleteIndex(TEST_INDEX_NAME); + await createDocs({ index: TEST_INDEX_NAME, - operations: docs.map((d) => `{"index": {}}\n${JSON.stringify(d)}\n`), + endDate: TEST_DOC_END_DATE, + docCount: TEST_DOC_COUNT, + dateSubstractUnit: 'days', }); - - log.info(`Indexed ${res.items.length} test data docs.`); - }; - - before(async () => { - await reset(); - await createDocs(); await reportingAPI.initLogs(); await common.navigateToApp('discover'); await discover.loadSavedSearch('Sparse Columns'); @@ -250,7 +326,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await reportingAPI.teardownLogs(); - await reset(); + await deleteIndex(TEST_INDEX_NAME); }); beforeEach(async () => {