Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this isn't an issue for classic mode because we don't use csv_v2 currently? If it's not used for classic mode atm, I wonder if there would be drawbacks to just always passing this regardless of mode? Just thinking it would prepare us if we ever migrated classic mode to csv_v2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can prepare or it can break current things 😀 Didn't want to change anything besides what is necessary to address the bug. We could revisit it later if becomes required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, definitely don't want to add risk to this bug fix 🙂 We can update it later if we migrate classic mode to csv_v2.

}
: params,
},
],
...searchSourceSharingData,
// CSV reports can be generated without a saved search so we provide a fallback title
title:
Expand Down
19 changes: 19 additions & 0 deletions x-pack/test/functional/apps/discover/__snapshots__/reporting.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

172 changes: 124 additions & 48 deletions x-pack/test/functional/apps/discover/reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<TestDoc>(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<TestDoc, 'timestamp' | 'name' | 'numberValue'> = {
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();
Expand All @@ -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 () => {
Expand Down Expand Up @@ -189,68 +264,69 @@ 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<TestDoc>(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');
});

after(async () => {
await reportingAPI.teardownLogs();
await reset();
await deleteIndex(TEST_INDEX_NAME);
});

beforeEach(async () => {
Expand Down