mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
# Backport This will backport the following commits from `main` to `8.18`: - [[Canvas/PDF report] Allow canvas to generate PDF report (#224309)](https://github.com/elastic/kibana/pull/224309) <!--- Backport version: 10.0.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Tim Sullivan","email":"tsullivan@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-06-17T22:47:22Z","message":"[Canvas/PDF report] Allow canvas to generate PDF report (#224309)\n\n## Summary\n\nCloses https://github.com/elastic/kibana/issues/224275\n\n**Context:** In https://github.com/elastic/kibana/pull/222273, we added\nrestrictions to the \"Reporting redirect app\" to make sure it could not\nbe abused by using unexpected locator types (such as the short URL\nlocator or the \"legacy\" locator type) when triggering redirects in the\nReporting headless browser. The restrictions are on the basis of a list\nof allowed locator types, which should be a list of every analytical app\nthat supports Reporting.\n\n**Problem:** Unfortunately that added a regression to Canvas PDF\nreporting, because the allow-list for locator types neglected to include\nthe Canvas locator type.\n\nThis PR solves the problem by adding the Canvas locator type to the set\nof allowed locator types.\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"f5dee1347f019d4e98a45070eeb80356453a9827","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:version","v8.15.6","v9.1.0","v8.19.0","v7.17.29","v9.0.3","v8.18.3","v8.17.8"],"title":"[Canvas/PDF report] Allow canvas to generate PDF report","number":224309,"url":"https://github.com/elastic/kibana/pull/224309","mergeCommit":{"message":"[Canvas/PDF report] Allow canvas to generate PDF report (#224309)\n\n## Summary\n\nCloses https://github.com/elastic/kibana/issues/224275\n\n**Context:** In https://github.com/elastic/kibana/pull/222273, we added\nrestrictions to the \"Reporting redirect app\" to make sure it could not\nbe abused by using unexpected locator types (such as the short URL\nlocator or the \"legacy\" locator type) when triggering redirects in the\nReporting headless browser. The restrictions are on the basis of a list\nof allowed locator types, which should be a list of every analytical app\nthat supports Reporting.\n\n**Problem:** Unfortunately that added a regression to Canvas PDF\nreporting, because the allow-list for locator types neglected to include\nthe Canvas locator type.\n\nThis PR solves the problem by adding the Canvas locator type to the set\nof allowed locator types.\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"f5dee1347f019d4e98a45070eeb80356453a9827"}},"sourceBranch":"main","suggestedTargetBranches":["8.15","7.17","9.0","8.18","8.17"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.6","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/224309","number":224309,"mergeCommit":{"message":"[Canvas/PDF report] Allow canvas to generate PDF report (#224309)\n\n## Summary\n\nCloses https://github.com/elastic/kibana/issues/224275\n\n**Context:** In https://github.com/elastic/kibana/pull/222273, we added\nrestrictions to the \"Reporting redirect app\" to make sure it could not\nbe abused by using unexpected locator types (such as the short URL\nlocator or the \"legacy\" locator type) when triggering redirects in the\nReporting headless browser. The restrictions are on the basis of a list\nof allowed locator types, which should be a list of every analytical app\nthat supports Reporting.\n\n**Problem:** Unfortunately that added a regression to Canvas PDF\nreporting, because the allow-list for locator types neglected to include\nthe Canvas locator type.\n\nThis PR solves the problem by adding the Canvas locator type to the set\nof allowed locator types.\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"f5dee1347f019d4e98a45070eeb80356453a9827"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/224335","number":224335,"state":"OPEN"},{"branch":"7.17","label":"v7.17.29","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.8","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
256 lines
9.2 KiB
TypeScript
256 lines
9.2 KiB
TypeScript
/*
|
|
* 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 fs from 'fs';
|
|
import path from 'path';
|
|
import type SuperTest from 'supertest';
|
|
import { format as formatUrl } from 'url';
|
|
import { promisify } from 'util';
|
|
|
|
import { INTERNAL_ROUTES, REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '@kbn/reporting-common';
|
|
import { FtrService } from '../ftr_provider_context';
|
|
|
|
const writeFileAsync = promisify(fs.writeFile);
|
|
const mkdirAsync = promisify(fs.mkdir);
|
|
|
|
export class ReportingPageObject extends FtrService {
|
|
private readonly browser = this.ctx.getService('browser');
|
|
private readonly config = this.ctx.getService('config');
|
|
private readonly log = this.ctx.getService('log');
|
|
private readonly retry = this.ctx.getService('retry');
|
|
private readonly security = this.ctx.getService('security');
|
|
private readonly testSubjects = this.ctx.getService('testSubjects');
|
|
private readonly find = this.ctx.getService('find');
|
|
private readonly share = this.ctx.getPageObject('share');
|
|
private readonly timePicker = this.ctx.getPageObject('timePicker');
|
|
|
|
async forceSharedItemsContainerSize({ width }: { width: number }) {
|
|
await this.browser.execute(`
|
|
var el = document.querySelector('[data-shared-items-container]');
|
|
el.style.flex="none";
|
|
el.style.width="${width}px";
|
|
`);
|
|
}
|
|
|
|
async getReportJobId(timeout: number): Promise<string> {
|
|
this.log.debug('getReportJobId');
|
|
|
|
try {
|
|
// get the report job id from a data attribute on the download button
|
|
const jobIdElement = await this.find.byCssSelector('[data-test-jobId]', timeout);
|
|
if (!jobIdElement) {
|
|
throw new Error('Failed to find report job id.');
|
|
}
|
|
const jobId = await jobIdElement.getAttribute('data-test-jobId');
|
|
if (!jobId) {
|
|
throw new Error('Failed to find report job id.');
|
|
}
|
|
return jobId;
|
|
} catch (err) {
|
|
let errorText = 'Unknown error';
|
|
if (await this.find.existsByCssSelector('[data-test-errorText]')) {
|
|
const errorTextEl = await this.find.byCssSelector('[data-test-errorText]');
|
|
errorText = (await errorTextEl.getAttribute('data-test-errorText')) ?? errorText;
|
|
}
|
|
throw new Error(`Test report failed: ${errorText}: ${err}`, { cause: err });
|
|
}
|
|
}
|
|
|
|
async getReportURL(timeout: number) {
|
|
this.log.debug('getReportURL');
|
|
|
|
try {
|
|
const url = await this.testSubjects.getAttribute(
|
|
'downloadCompletedReportButton',
|
|
'href',
|
|
timeout
|
|
);
|
|
this.log.debug(`getReportURL got url: ${url}`);
|
|
|
|
return url;
|
|
} catch (err) {
|
|
const errorTextEl = await this.find.byCssSelector('[data-test-errorText]');
|
|
const errorText = await errorTextEl.getAttribute('data-test-errorText');
|
|
const newError = new Error(`Test report failed: ${errorText}: ${err}`);
|
|
throw newError;
|
|
}
|
|
}
|
|
|
|
async removeForceSharedItemsContainerSize() {
|
|
await this.browser.execute(`
|
|
var el = document.querySelector('[data-shared-items-container]');
|
|
el.style.flex = null;
|
|
el.style.width = null;
|
|
`);
|
|
}
|
|
|
|
async getResponse(fullUrl: string): Promise<SuperTest.Response> {
|
|
this.log.debug(`getResponse for ${fullUrl}`);
|
|
const kibanaServerConfig = this.config.get('servers.kibana');
|
|
const baseURL = formatUrl({
|
|
...kibanaServerConfig,
|
|
auth: false,
|
|
});
|
|
const urlWithoutBase = fullUrl.replace(baseURL, '');
|
|
const res = await this.security.testUserSupertest.get(urlWithoutBase);
|
|
return res ?? '';
|
|
}
|
|
|
|
async getReportInfo(jobId: string) {
|
|
this.log.debug(`getReportInfo for ${jobId}`);
|
|
const response = await this.getResponse(INTERNAL_ROUTES.JOBS.INFO_PREFIX + `/${jobId}`);
|
|
return response.body;
|
|
}
|
|
|
|
async getRawReportData(url: string): Promise<Buffer> {
|
|
this.log.debug(`getRawReportData for ${url}`);
|
|
const response = await this.getResponse(url);
|
|
expect(response.body).to.be.a(Buffer);
|
|
return response.body as Buffer;
|
|
}
|
|
|
|
async openShareMenuItem(itemTitle: string) {
|
|
this.log.debug(`openShareMenuItem title:${itemTitle}`);
|
|
const isShareMenuOpen = await this.testSubjects.exists('shareContextMenu');
|
|
if (!isShareMenuOpen) {
|
|
await this.testSubjects.click('shareTopNavButton');
|
|
} else {
|
|
// there is no easy way to ensure the menu is at the top level
|
|
// so just close the existing menu
|
|
await this.testSubjects.click('shareTopNavButton');
|
|
// and then re-open the menu
|
|
await this.testSubjects.click('shareTopNavButton');
|
|
}
|
|
const menuPanel = await this.find.byCssSelector('div.euiContextMenuPanel');
|
|
await this.testSubjects.click(`sharePanel-${itemTitle.replace(' ', '')}`);
|
|
await this.testSubjects.waitForDeleted(menuPanel);
|
|
}
|
|
|
|
async openExportTab() {
|
|
this.log.debug('open export modal');
|
|
await this.share.clickTab('Export');
|
|
}
|
|
|
|
async getQueueReportError() {
|
|
return await this.testSubjects.exists('errorToastMessage');
|
|
}
|
|
|
|
async getGenerateReportButton() {
|
|
return await this.retry.try(async () => await this.testSubjects.find('generateReportButton'));
|
|
}
|
|
|
|
async isGenerateReportButtonDisabled() {
|
|
const generateReportButton = await this.getGenerateReportButton();
|
|
return await this.retry.try(async () => {
|
|
const isDisabled = await generateReportButton.getAttribute('disabled');
|
|
return isDisabled;
|
|
});
|
|
}
|
|
|
|
async canReportBeCreated() {
|
|
await this.clickGenerateReportButton();
|
|
const success = await this.checkForReportingToasts();
|
|
return success;
|
|
}
|
|
|
|
async checkUsePrintLayout() {
|
|
// The print layout checkbox slides in as part of an animation, and tests can
|
|
// attempt to click it too quickly, leading to flaky tests. The 500ms wait allows
|
|
// the animation to complete before we attempt a click.
|
|
const menuAnimationDelay = 500;
|
|
await this.retry.tryForTime(menuAnimationDelay, () =>
|
|
this.testSubjects.click('usePrintLayout')
|
|
);
|
|
}
|
|
|
|
async clickGenerateReportButton() {
|
|
await this.testSubjects.click('generateReportButton');
|
|
}
|
|
|
|
async toggleReportMode() {
|
|
await this.testSubjects.click('reportModeToggle');
|
|
}
|
|
|
|
async checkForReportingToasts() {
|
|
this.log.debug('Reporting:checkForReportingToasts');
|
|
const isToastPresent = await this.testSubjects.exists('completeReportSuccess', {
|
|
allowHidden: true,
|
|
timeout: 90000,
|
|
});
|
|
// Close toast so it doesn't obscure the UI.
|
|
if (isToastPresent) {
|
|
await this.testSubjects.click('completeReportSuccess > toastCloseButton');
|
|
}
|
|
|
|
return isToastPresent;
|
|
}
|
|
|
|
// set the time picker to a range matching 720 documents when using the
|
|
// reporting/ecommerce archive
|
|
async setTimepickerInEcommerceDataRange() {
|
|
this.log.debug('Reporting:setTimepickerInDataRange');
|
|
const fromTime = 'Jun 20, 2019 @ 00:00:00.000';
|
|
const toTime = 'Jun 25, 2019 @ 00:00:00.000';
|
|
await this.timePicker.setAbsoluteRange(fromTime, toTime);
|
|
}
|
|
|
|
// set the time picker to a range matching 0 documents when using the
|
|
// reporting/ecommerce archive
|
|
async setTimepickerInEcommerceNoDataRange() {
|
|
this.log.debug('Reporting:setTimepickerInNoDataRange');
|
|
const fromTime = 'Sep 19, 1999 @ 06:31:44.000';
|
|
const toTime = 'Sep 23, 1999 @ 18:31:44.000';
|
|
await this.timePicker.setAbsoluteRange(fromTime, toTime);
|
|
}
|
|
|
|
async getManagementList() {
|
|
const table = await this.testSubjects.find(REPORT_TABLE_ID);
|
|
const allRows = await table.findAllByTestSubject(REPORT_TABLE_ROW_ID);
|
|
|
|
return await Promise.all(
|
|
allRows.map(async (row) => {
|
|
const $ = await row.parseDomContent();
|
|
return {
|
|
report: $.findTestSubject('reportingListItemObjectTitle').text().trim(),
|
|
createdAt: $.findTestSubject('reportJobCreatedAt').text().trim(),
|
|
status: $.findTestSubject('reportJobStatus').text().trim(),
|
|
actions: $.findTestSubject('reportJobActions').text().trim(),
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
async openReportFlyout(reportTitle: string) {
|
|
const table = await this.testSubjects.find(REPORT_TABLE_ID);
|
|
const allRows = await table.findAllByTestSubject(REPORT_TABLE_ROW_ID);
|
|
for (const row of allRows) {
|
|
const titleColumn = await row.findByTestSubject('reportingListItemObjectTitle');
|
|
const title = await titleColumn.getVisibleText();
|
|
if (title === reportTitle) {
|
|
await titleColumn.click();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
async writeSessionReport(name: string, reportExt: string, rawPdf: Buffer, folder: string) {
|
|
const sessionDirectory = path.resolve(folder, 'session');
|
|
await mkdirAsync(sessionDirectory, { recursive: true });
|
|
const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`);
|
|
await writeFileAsync(sessionReportPath, rawPdf);
|
|
this.log.debug(`sessionReportPath (${sessionReportPath})`);
|
|
return sessionReportPath;
|
|
}
|
|
|
|
getBaselineReportPath(fileName: string, reportExt: string, folder: string) {
|
|
const baselineFolder = path.resolve(folder, 'baseline');
|
|
const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`);
|
|
this.log.debug(`baselineReportPath (${fullPath})`);
|
|
return fullPath;
|
|
}
|
|
}
|