[Reporting] add version to all export types job params (#106137)

* add version to csv params

* fix ts

* fix api tests

* use kibana version from packageInfo

* use kibana version from packageInfo

* clean up ide warnings

* utility to log and set a default params version

* fix baseparams ts

* update snapshot

* check version in enqueue job

* add temporary ts-ignore for canvas

* clarify comment

* fix hardcoded version in png_pdf_panel

* clarify the UNVERSIONED_VERSION variable with a comment

* fix canvas jest test

* fix ts in example app

* fix types

* send version param to canvas util for job params

* update jest snapshot

* Update utils.test.ts

* fix snapshot

* remove browserTimezone and version from integration boilerplate

* wip ensure version is always populated in job params inside of the service

* wip2

* wip3

* wip4

* wip5

* wip6

* update note

* update example plugin

* wip7

* improve tests

* fix dynamic job params

* better testing

* improve enqueue_job test

* more tests

* fix types

* fix types

* fix example ts

* simplify props

* fix test

* --wip-- [skip ci]

* consolidate baseparams back into one interface

* fix rison encoding of apiClient param

* clean up

* reorganize imports

* back out functional change

* fix 400 error in download csv

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2021-08-02 10:00:37 -07:00 committed by GitHub
parent 81fd64c838
commit 5e8b24230a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 493 additions and 353 deletions

View file

@ -8,13 +8,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
import { SetupDeps, StartDeps } from './types';
import { ReportingExampleApp } from './components/app';
import { SetupDeps, StartDeps } from './types';
export const renderApp = (
coreStart: CoreStart,
deps: Omit<StartDeps & SetupDeps, 'developerExamples'>,
{ appBasePath, element }: AppMountParameters
{ appBasePath, element }: AppMountParameters // FIXME: appBasePath is deprecated
) => {
ReactDOM.render(<ReportingExampleApp basename={appBasePath} {...coreStart} {...deps} />, element);

View file

@ -28,16 +28,10 @@ import { BrowserRouter as Router } from 'react-router-dom';
import * as Rx from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public';
import { CoreStart } from '../../../../../src/core/public';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public';
import { JobParamsPDF } from '../../../../plugins/reporting/server/export_types/printable_pdf/types';
interface ReportingExampleAppDeps {
interface ReportingExampleAppProps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
navigation: NavigationPublicPluginStart;
reporting: ReportingStart;
screenshotMode: ScreenshotModePluginSetup;
}
@ -46,11 +40,9 @@ const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana'];
export const ReportingExampleApp = ({
basename,
notifications,
http,
reporting,
screenshotMode,
}: ReportingExampleAppDeps) => {
}: ReportingExampleAppProps) => {
const { getDefaultLayoutSelectors } = reporting;
// Context Menu
@ -74,7 +66,7 @@ export const ReportingExampleApp = ({
});
});
const getPDFJobParamsDefault = (): JobParamsPDF => {
const getPDFJobParamsDefault = () => {
return {
layout: {
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,

View file

@ -22,7 +22,6 @@ test('getPdfJobParams returns the correct job params for canvas layout', () => {
const jobParams = getPdfJobParams(workpadSharingData, basePath);
expect(jobParams).toMatchInlineSnapshot(`
Object {
"browserTimezone": "America/New_York",
"layout": Object {
"dimensions": Object {
"height": 0,

View file

@ -6,9 +6,7 @@
*/
import { IBasePath } from 'kibana/public';
import moment from 'moment-timezone';
import rison from 'rison-node';
import { BaseParams } from '../../../../../reporting/common/types';
import { CanvasWorkpad } from '../../../../types';
export interface CanvasWorkpadSharingData {
@ -16,13 +14,10 @@ export interface CanvasWorkpadSharingData {
pageCount: number;
}
// TODO: get the correct type from Reporting plugin
type JobParamsPDF = BaseParams & { relativeUrls: string[] };
export function getPdfJobParams(
{ workpad: { id, name: title, width, height }, pageCount }: CanvasWorkpadSharingData,
basePath: IBasePath
): JobParamsPDF {
) {
const urlPrefix = basePath.get().replace(basePath.serverBasePath, ''); // for Spaces prefix, which is included in basePath.get()
const canvasEntry = `${urlPrefix}/app/canvas#`;
@ -43,7 +38,6 @@ export function getPdfJobParams(
}
return {
browserTimezone: moment.tz.guess(),
layout: {
dimensions: { width, height },
id: 'canvas',

View file

@ -111,6 +111,11 @@ export enum JOB_STATUSES {
export const REPORT_TABLE_ID = 'reportJobListing';
export const REPORT_TABLE_ROW_ID = 'reportJobRow';
// Job params require a `version` field as of 7.15.0. For older jobs set with
// automation that have no version value in the job params, we assume the
// intended version is 7.14.0
export const UNVERSIONED_VERSION = '7.14.0';
// hacky endpoint: download CSV without queueing a report
// FIXME: find a way to make these endpoints "generic" instead of hardcoded, as are the queued report export types
export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`;

View file

@ -97,10 +97,11 @@ export interface ReportDocument extends ReportDocumentHead {
}
export interface BaseParams {
browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface
layout?: LayoutParams;
objectType: string;
title: string;
browserTimezone: string; // to format dates in the user's time zone
version: string; // to handle any state migrations
}
export type JobId = string;

View file

@ -6,34 +6,45 @@
*/
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { stringify } from 'query-string';
import rison from 'rison-node';
import { HttpSetup } from 'src/core/public';
import rison, { RisonObject } from 'rison-node';
import { HttpSetup, IUiSettingsClient } from 'src/core/public';
import {
API_BASE_GENERATE,
API_BASE_URL,
API_GENERATE_IMMEDIATE,
API_LIST_URL,
API_MIGRATE_ILM_POLICY_URL,
REPORTING_MANAGEMENT_HOME,
} from '../../../common/constants';
import { DownloadReportFn, JobId, ManagementLinkFn, ReportApiJSON } from '../../../common/types';
import {
BaseParams,
DownloadReportFn,
JobId,
ManagementLinkFn,
ReportApiJSON,
} from '../../../common/types';
import { add } from '../../notifier/job_completion_notifications';
import { Job } from '../job';
/*
* For convenience, apps do not have to provide the browserTimezone and Kibana version.
* Those fields are added in this client as part of the service.
* TODO: export a type like this to other plugins: https://github.com/elastic/kibana/issues/107085
*/
type AppParams = Omit<BaseParams, 'browserTimezone' | 'version'>;
export interface DiagnoseResponse {
help: string[];
success: boolean;
logs: string;
}
interface JobParams {
[paramName: string]: any;
}
interface IReportingAPI {
// Helpers
getReportURL(jobId: string): string;
getReportingJobPath(exportType: string, jobParams: JobParams): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL
getReportingJobPath<T>(exportType: string, jobParams: BaseParams & T): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL
createReportingJob(exportType: string, jobParams: any): Promise<Job>; // Sends a request to queue a job, with the job params in the POST body
getServerBasePath(): string; // Provides the raw server basePath to allow it to be stripped out from relativeUrls in job params
@ -57,11 +68,11 @@ interface IReportingAPI {
}
export class ReportingAPIClient implements IReportingAPI {
private http: HttpSetup;
constructor(http: HttpSetup) {
this.http = http;
}
constructor(
private http: HttpSetup,
private uiSettings: IUiSettingsClient,
private kibanaVersion: string
) {}
public getReportURL(jobId: string) {
const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL);
@ -132,13 +143,15 @@ export class ReportingAPIClient implements IReportingAPI {
return reports.map((report) => new Job(report));
}
public getReportingJobPath(exportType: string, jobParams: JobParams) {
const params = stringify({ jobParams: rison.encode(jobParams) });
public getReportingJobPath(exportType: string, jobParams: BaseParams) {
const risonObject: RisonObject = jobParams as Record<string, any>;
const params = stringify({ jobParams: rison.encode(risonObject) });
return `${this.http.basePath.prepend(API_BASE_GENERATE)}/${exportType}?${params}`;
}
public async createReportingJob(exportType: string, jobParams: any) {
const jobParamsRison = rison.encode(jobParams);
public async createReportingJob(exportType: string, jobParams: BaseParams) {
const risonObject: RisonObject = jobParams as Record<string, any>;
const jobParamsRison = rison.encode(risonObject);
const resp: { job: ReportApiJSON } = await this.http.post(
`${API_BASE_GENERATE}/${exportType}`,
{
@ -154,6 +167,27 @@ export class ReportingAPIClient implements IReportingAPI {
return new Job(resp.job);
}
public async createImmediateReport(baseParams: BaseParams) {
const { objectType: _objectType, ...params } = baseParams; // objectType is not needed for immediate download api
return this.http.post(`${API_GENERATE_IMMEDIATE}`, { body: JSON.stringify(params) });
}
public getDecoratedJobParams<T extends AppParams>(baseParams: T): BaseParams {
// If the TZ is set to the default "Browser", it will not be useful for
// server-side export. We need to derive the timezone and pass it as a param
// to the export API.
const browserTimezone: string =
this.uiSettings.get('dateFormat:tz') === 'Browser'
? moment.tz.guess()
: this.uiSettings.get('dateFormat:tz');
return {
browserTimezone,
version: this.kibanaVersion,
...baseParams,
};
}
public getManagementLink: ManagementLinkFn = () =>
this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME);

View file

@ -26,7 +26,8 @@ const mockJobsFound: Job[] = [
{ id: 'job-source-mock3', status: 'pending', output: { csv_contains_formulas: false, max_size_reached: false }, payload: { title: 'specimen' } },
].map((j) => new Job(j as ReportApiJSON)); // prettier-ignore
const jobQueueClientMock = new ReportingAPIClient(coreMock.createSetup().http);
const coreSetup = coreMock.createSetup();
const jobQueueClientMock = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0');
jobQueueClientMock.findForJobIds = async () => mockJobsFound;
jobQueueClientMock.getInfo = () =>
Promise.resolve(({ content: 'this is the completed report data' } as unknown) as Job);

View file

@ -7,6 +7,7 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { coreMock } from '../../../../../src/core/public/mocks';
import { Job } from '../lib/job';
import { ReportInfoButton } from './report_info_button';
@ -14,8 +15,9 @@ jest.mock('../lib/reporting_api_client');
import { ReportingAPIClient } from '../lib/reporting_api_client';
const httpSetup = {} as any;
const apiClient = new ReportingAPIClient(httpSetup);
const coreSetup = coreMock.createSetup();
const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0');
const job = new Job({
id: 'abc-123',
index: '.reporting-2020.04.12',
@ -29,6 +31,7 @@ const job = new Job({
meta: { layout: 'preserve_layout', objectType: 'canvas workpad' },
payload: {
browserTimezone: 'America/Phoenix',
version: '7.15.0-test',
layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' },
objectType: 'canvas workpad',
title: 'My Canvas Workpad',

View file

@ -32,15 +32,15 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
});
const mockJobs: ReportApiJSON[] = [
{ id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000 }, // prettier-ignore
{ id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 },
{ id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 },
{ id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 },
{ id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 },
{ id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 },
{ id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 },
{ id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 },
{ id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 },
{ id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000}, // prettier-ignore
{ id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 },
{ id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 },
{ id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded' ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 },
{ id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 },
{ id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 },
{ id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 },
{ id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 },
{ id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count', version: '7.14.0' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 },
]; // prettier-ignore
const reportingAPIClient = {

View file

@ -6,6 +6,7 @@
*/
import { coreMock } from 'src/core/public/mocks';
import { ReportingAPIClient } from './lib/reporting_api_client';
import { ReportingSetup } from '.';
import { getDefaultLayoutSelectors } from '../common';
import { getSharedComponents } from './shared';
@ -14,10 +15,11 @@ type Setup = jest.Mocked<ReportingSetup>;
const createSetupContract = (): Setup => {
const coreSetup = coreMock.createSetup();
const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0');
return {
getDefaultLayoutSelectors: jest.fn().mockImplementation(getDefaultLayoutSelectors),
usesUiCapabilities: jest.fn().mockImplementation(() => true),
components: getSharedComponents(coreSetup),
components: getSharedComponents(coreSetup, apiClient),
};
};

View file

@ -8,13 +8,17 @@
import * as Rx from 'rxjs';
import { first } from 'rxjs/operators';
import { CoreStart } from 'src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
import { LicensingPluginSetup } from '../../../licensing/public';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ReportingCsvPanelAction } from './get_csv_panel_action';
type LicenseResults = 'valid' | 'invalid' | 'unavailable' | 'expired';
const core = coreMock.createSetup();
let apiClient: ReportingAPIClient;
describe('GetCsvReportPanelAction', () => {
let core: any;
let context: any;
let mockLicense$: any;
let mockSearchSource: any;
@ -32,6 +36,9 @@ describe('GetCsvReportPanelAction', () => {
});
beforeEach(() => {
apiClient = new ReportingAPIClient(core.http, core.uiSettings, '7.15.0');
jest.spyOn(apiClient, 'createImmediateReport');
mockLicense$ = (state: LicenseResults = 'valid') => {
return (Rx.of({
check: jest.fn().mockImplementation(() => ({ state })),
@ -47,21 +54,6 @@ describe('GetCsvReportPanelAction', () => {
null,
];
core = {
http: {
post: jest.fn().mockImplementation(() => Promise.resolve(true)),
},
notifications: {
toasts: {
addSuccess: jest.fn(),
addDanger: jest.fn(),
},
},
uiSettings: {
get: () => 'Browser',
},
} as any;
mockSearchSource = {
createCopy: () => mockSearchSource,
removeField: jest.fn(),
@ -92,6 +84,7 @@ describe('GetCsvReportPanelAction', () => {
it('translates empty embeddable context into job params', async () => {
const panel = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -101,12 +94,14 @@ describe('GetCsvReportPanelAction', () => {
await panel.execute(context);
expect(core.http.post).toHaveBeenCalledWith(
'/api/reporting/v1/generate/immediate/csv_searchsource',
{
body: '{"searchSource":{},"columns":[],"browserTimezone":"America/New_York"}',
}
);
expect(apiClient.createImmediateReport).toHaveBeenCalledWith({
browserTimezone: undefined,
columns: [],
objectType: 'downloadCsv',
searchSource: {},
title: undefined,
version: '7.15.0',
});
});
it('translates embeddable context into job params', async () => {
@ -126,6 +121,7 @@ describe('GetCsvReportPanelAction', () => {
const panel = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -135,18 +131,20 @@ describe('GetCsvReportPanelAction', () => {
await panel.execute(context);
expect(core.http.post).toHaveBeenCalledWith(
'/api/reporting/v1/generate/immediate/csv_searchsource',
{
body:
'{"searchSource":{"testData":"testDataValue"},"columns":["column_a","column_b"],"browserTimezone":"America/New_York"}',
}
);
expect(apiClient.createImmediateReport).toHaveBeenCalledWith({
browserTimezone: undefined,
columns: ['column_a', 'column_b'],
objectType: 'downloadCsv',
searchSource: { testData: 'testDataValue' },
title: undefined,
version: '7.15.0',
});
});
it('allows downloading for valid licenses', async () => {
const panel = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -162,6 +160,7 @@ describe('GetCsvReportPanelAction', () => {
it('shows a good old toastie when it successfully starts', async () => {
const panel = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -176,14 +175,10 @@ describe('GetCsvReportPanelAction', () => {
});
it('shows a bad old toastie when it successfully fails', async () => {
const coreFails = {
...core,
http: {
post: jest.fn().mockImplementation(() => Promise.reject('No more ram!')),
},
};
apiClient.createImmediateReport = jest.fn().mockRejectedValue('No more ram!');
const panel = new ReportingCsvPanelAction({
core: coreFails,
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -200,6 +195,7 @@ describe('GetCsvReportPanelAction', () => {
const licenseMock$ = mockLicense$('invalid');
const plugin = new ReportingCsvPanelAction({
core,
apiClient,
license$: licenseMock$,
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -215,6 +211,7 @@ describe('GetCsvReportPanelAction', () => {
it('sets a display and icon type', () => {
const panel = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -230,6 +227,7 @@ describe('GetCsvReportPanelAction', () => {
it(`doesn't allow downloads when UI capability is not enabled`, async () => {
const plugin = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -248,6 +246,7 @@ describe('GetCsvReportPanelAction', () => {
mockStartServices$ = new Rx.Subject();
const plugin = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: true,
@ -261,6 +260,7 @@ describe('GetCsvReportPanelAction', () => {
it(`allows download when license is valid and deprecated roles config is enabled`, async () => {
const plugin = new ReportingCsvPanelAction({
core,
apiClient,
license$: mockLicense$(),
startServices$: mockStartServices$,
usesUiCapabilities: false,

View file

@ -6,9 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
import moment from 'moment-timezone';
import * as Rx from 'rxjs';
import type { CoreSetup } from 'src/core/public';
import type { CoreSetup, IUiSettingsClient, NotificationsSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public';
import {
@ -20,9 +19,9 @@ import { ViewMode } from '../../../../../src/plugins/embeddable/public';
import type { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public';
import { IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public';
import type { LicensingPluginSetup } from '../../../licensing/public';
import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants';
import type { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types';
import { CSV_REPORTING_ACTION } from '../../common/constants';
import { checkLicense } from '../lib/license_check';
import { ReportingAPIClient } from '../lib/reporting_api_client';
function isSavedSearchEmbeddable(
embeddable: IEmbeddable | ISearchEmbeddable
@ -35,6 +34,7 @@ interface ActionContext {
}
interface Params {
apiClient: ReportingAPIClient;
core: CoreSetup;
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
license$: LicensingPluginSetup['license$'];
@ -47,11 +47,16 @@ export class ReportingCsvPanelAction implements ActionDefinition<ActionContext>
public readonly id = CSV_REPORTING_ACTION;
private licenseHasDownloadCsv: boolean = false;
private capabilityHasDownloadCsv: boolean = false;
private core: CoreSetup;
private uiSettings: IUiSettingsClient;
private notifications: NotificationsSetup;
private apiClient: ReportingAPIClient;
constructor({ core, startServices$, license$, usesUiCapabilities }: Params) {
constructor({ core, startServices$, license$, usesUiCapabilities, apiClient }: Params) {
this.isDownloading = false;
this.core = core;
this.uiSettings = core.uiSettings;
this.notifications = core.notifications;
this.apiClient = apiClient;
license$.subscribe((license) => {
const results = license.check('reporting', 'basic');
@ -83,7 +88,7 @@ export class ReportingCsvPanelAction implements ActionDefinition<ActionContext>
return await getSharingData(
savedSearch.searchSource,
savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977
this.core.uiSettings
this.uiSettings
);
}
@ -111,24 +116,16 @@ export class ReportingCsvPanelAction implements ActionDefinition<ActionContext>
const savedSearch = embeddable.getSavedSearch();
const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable);
// If the TZ is set to the default "Browser", it will not be useful for
// server-side export. We need to derive the timezone and pass it as a param
// to the export API.
// TODO: create a helper utility in Reporting. This is repeated in a few places.
const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz');
const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone;
const immediateJobParams: JobParamsDownloadCSV = {
const immediateJobParams = this.apiClient.getDecoratedJobParams({
searchSource,
columns,
browserTimezone,
title: savedSearch.title,
};
const body = JSON.stringify(immediateJobParams);
objectType: 'downloadCsv', // FIXME: added for typescript, but immediate download job does not need objectType
});
this.isDownloading = true;
this.core.notifications.toasts.addSuccess({
this.notifications.toasts.addSuccess({
title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', {
defaultMessage: `CSV Download Started`,
}),
@ -138,9 +135,9 @@ export class ReportingCsvPanelAction implements ActionDefinition<ActionContext>
'data-test-subj': 'csvDownloadStarted',
});
await this.core.http
.post(`${API_GENERATE_IMMEDIATE}`, { body })
.then((rawResponse: string) => {
await this.apiClient
.createImmediateReport(immediateJobParams)
.then((rawResponse) => {
this.isDownloading = false;
const download = `${savedSearch.title}.csv`;
@ -166,7 +163,7 @@ export class ReportingCsvPanelAction implements ActionDefinition<ActionContext>
private onGenerationFail(error: Error) {
this.isDownloading = false;
this.core.notifications.toasts.addDanger({
this.notifications.toasts.addDanger({
title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', {
defaultMessage: `CSV download failed`,
}),

View file

@ -11,6 +11,8 @@ import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
HttpSetup,
IUiSettingsClient,
NotificationsSetup,
Plugin,
PluginInitializerContext,
@ -32,15 +34,14 @@ import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_ha
import { getGeneralErrorToast } from './notifier';
import { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action';
import { getSharedComponents } from './shared';
import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
import type {
SharePluginSetup,
SharePluginStart,
UiActionsSetup,
UiActionsStart,
} from './shared_imports';
import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
export interface ClientConfigType {
poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } };
@ -89,6 +90,8 @@ export class ReportingPublicPlugin
ReportingPublicPluginSetupDendencies,
ReportingPublicPluginStartDendencies
> {
private kibanaVersion: string;
private apiClient?: ReportingAPIClient;
private readonly stop$ = new Rx.ReplaySubject(1);
private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', {
defaultMessage: 'Reporting',
@ -101,6 +104,17 @@ export class ReportingPublicPlugin
constructor(initializerContext: PluginInitializerContext) {
this.config = initializerContext.config.get<ClientConfigType>();
this.kibanaVersion = initializerContext.env.packageInfo.version;
}
/*
* Use a single instance of ReportingAPIClient for all the reporting code
*/
private getApiClient(http: HttpSetup, uiSettings: IUiSettingsClient) {
if (!this.apiClient) {
this.apiClient = new ReportingAPIClient(http, uiSettings, this.kibanaVersion);
}
return this.apiClient;
}
private getContract(core?: CoreSetup) {
@ -108,7 +122,7 @@ export class ReportingPublicPlugin
this.contract = {
getDefaultLayoutSelectors,
usesUiCapabilities: () => this.config.roles?.enabled === false,
components: getSharedComponents(core),
components: getSharedComponents(core, this.getApiClient(core.http, core.uiSettings)),
};
}
@ -120,11 +134,11 @@ export class ReportingPublicPlugin
}
public setup(core: CoreSetup, setupDeps: ReportingPublicPluginSetupDendencies) {
const { http, getStartServices, uiSettings } = core;
const { getStartServices, uiSettings } = core;
const {
home,
management,
licensing: { license$ },
licensing: { license$ }, // FIXME: 'license$' is deprecated
share,
uiActions,
} = setupDeps;
@ -132,7 +146,7 @@ export class ReportingPublicPlugin
const startServices$ = Rx.from(getStartServices());
const usesUiCapabilities = !this.config.roles.enabled;
const apiClient = new ReportingAPIClient(http);
const apiClient = this.getApiClient(core.http, core.uiSettings);
home.featureCatalogue.register({
id: 'reporting',
@ -181,7 +195,7 @@ export class ReportingPublicPlugin
uiActions.addTriggerAction(
CONTEXT_MENU_TRIGGER,
new ReportingCsvPanelAction({ core, startServices$, license$, usesUiCapabilities })
new ReportingCsvPanelAction({ core, apiClient, startServices$, license$, usesUiCapabilities })
);
const reportingStart = this.getContract(core);
@ -213,8 +227,8 @@ export class ReportingPublicPlugin
}
public start(core: CoreStart) {
const { http, notifications } = core;
const apiClient = new ReportingAPIClient(http);
const { notifications } = core;
const apiClient = this.getApiClient(core.http, core.uiSettings);
const streamHandler = new StreamHandler(notifications, apiClient);
const interval = durationToNumber(this.config.poll.jobsRefresh.interval);
Rx.timer(0, interval)

View file

@ -349,7 +349,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%29"
textToCopy="http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%2Cversion%3A%277.15.0%27%29"
>
<EuiToolTip
anchorClassName="eui-displayBlock"
@ -787,7 +787,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%29"
textToCopy="http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%2Cversion%3A%277.15.0%27%29"
>
<EuiToolTip
anchorClassName="eui-displayBlock"
@ -1097,7 +1097,7 @@ exports[`ScreenCapturePanelContent renders the default view properly 1`] = `
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%29"
textToCopy="http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%2Cversion%3A%277.15.0%27%29"
>
<EuiToolTip
anchorClassName="eui-displayBlock"

View file

@ -0,0 +1,33 @@
/*
* 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 * as Rx from 'rxjs';
import type { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import type { LicensingPluginSetup } from '../../../licensing/public';
import type { LayoutParams } from '../../common/types';
import type { ReportingAPIClient } from '../lib/reporting_api_client';
export interface ExportPanelShareOpts {
apiClient: ReportingAPIClient;
toasts: ToastsSetup;
uiSettings: IUiSettingsClient;
license$: LicensingPluginSetup['license$']; // FIXME: 'license$' is deprecated
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
usesUiCapabilities: boolean;
}
export interface ReportingSharingData {
title: string;
layout: LayoutParams;
}
export interface JobParamsProviderOptions {
sharingData: ReportingSharingData;
shareableUrl: string;
objectType: string;
}

View file

@ -6,35 +6,22 @@
*/
import { i18n } from '@kbn/i18n';
import moment from 'moment-timezone';
import React from 'react';
import * as Rx from 'rxjs';
import type { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import type { SearchSourceFields } from 'src/plugins/data/common';
import { ExportPanelShareOpts } from '.';
import type { ShareContext } from '../../../../../src/plugins/share/public';
import type { LicensingPluginSetup } from '../../../licensing/public';
import { CSV_JOB_TYPE } from '../../common/constants';
import type { JobParamsCSV } from '../../server/export_types/csv_searchsource/types';
import { checkLicense } from '../lib/license_check';
import type { ReportingAPIClient } from '../lib/reporting_api_client';
import { ReportingPanelContent } from './reporting_panel_content_lazy';
export const ReportingCsvShareProvider = ({
apiClient,
toasts,
uiSettings,
license$,
startServices$,
uiSettings,
usesUiCapabilities,
}: {
apiClient: ReportingAPIClient;
toasts: ToastsSetup;
license$: LicensingPluginSetup['license$'];
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
uiSettings: IUiSettingsClient;
usesUiCapabilities: boolean;
}) => {
}: ExportPanelShareOpts) => {
let licenseToolTipContent = '';
let licenseHasCsvReporting = false;
let licenseDisabled = true;
@ -56,22 +43,12 @@ export const ReportingCsvShareProvider = ({
capabilityHasCsvReporting = true; // deprecated
}
// If the TZ is set to the default "Browser", it will not be useful for
// server-side export. We need to derive the timezone and pass it as a param
// to the export API.
// TODO: create a helper utility in Reporting. This is repeated in a few places.
const browserTimezone =
uiSettings.get('dateFormat:tz') === 'Browser'
? moment.tz.guess()
: uiSettings.get('dateFormat:tz');
const getShareMenuItems = ({ objectType, objectId, sharingData, onClose }: ShareContext) => {
if ('search' !== objectType) {
return [];
}
const jobParams: JobParamsCSV = {
browserTimezone,
const jobParams = {
title: sharingData.title as string,
objectType,
searchSource: sharingData.searchSource as SearchSourceFields,
@ -104,6 +81,7 @@ export const ReportingCsvShareProvider = ({
requiresSavedState={false}
apiClient={apiClient}
toasts={toasts}
uiSettings={uiSettings}
reportType={CSV_JOB_TYPE}
layoutId={undefined}
objectId={objectId}

View file

@ -6,83 +6,53 @@
*/
import { i18n } from '@kbn/i18n';
import moment from 'moment-timezone';
import React from 'react';
import * as Rx from 'rxjs';
import type { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import { ShareContext } from 'src/plugins/share/public';
import type { LicensingPluginSetup } from '../../../licensing/public';
import type { LayoutParams } from '../../common/types';
import type { JobParamsPNG } from '../../server/export_types/png/types';
import type { JobParamsPDF } from '../../server/export_types/printable_pdf/types';
import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
import { checkLicense } from '../lib/license_check';
import type { ReportingAPIClient } from '../lib/reporting_api_client';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
interface JobParamsProviderOptions {
shareableUrl: string;
apiClient: ReportingAPIClient;
objectType: string;
browserTimezone: string;
sharingData: Record<string, unknown>;
}
const jobParamsProvider = ({
objectType,
browserTimezone,
sharingData,
}: JobParamsProviderOptions) => {
return {
const getJobParams = (
apiClient: ReportingAPIClient,
opts: JobParamsProviderOptions,
type: 'pdf' | 'png'
) => () => {
const {
objectType,
browserTimezone,
layout: sharingData.layout as LayoutParams,
title: sharingData.title as string,
};
};
sharingData: { title, layout },
} = opts;
const baseParams = {
objectType,
layout,
title,
};
const getPdfJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDF => {
// Relative URL must have URL prefix (Spaces ID prefix), but not server basePath
// Replace hashes with original RISON values.
const relativeUrl = opts.shareableUrl.replace(
window.location.origin + opts.apiClient.getServerBasePath(),
window.location.origin + apiClient.getServerBasePath(),
''
);
return {
...jobParamsProvider(opts),
relativeUrls: [relativeUrl], // multi URL for PDF
};
};
if (type === 'pdf') {
// multi URL for PDF
return { ...baseParams, relativeUrls: [relativeUrl] };
}
const getPngJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPNG => {
// Replace hashes with original RISON values.
const relativeUrl = opts.shareableUrl.replace(
window.location.origin + opts.apiClient.getServerBasePath(),
''
);
return {
...jobParamsProvider(opts),
relativeUrl, // single URL for PNG
};
// single URL for PNG
return { ...baseParams, relativeUrl };
};
export const reportingScreenshotShareProvider = ({
apiClient,
toasts,
uiSettings,
license$,
startServices$,
uiSettings,
usesUiCapabilities,
}: {
apiClient: ReportingAPIClient;
toasts: ToastsSetup;
license$: LicensingPluginSetup['license$'];
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
uiSettings: IUiSettingsClient;
usesUiCapabilities: boolean;
}) => {
}: ExportPanelShareOpts) => {
let licenseToolTipContent = '';
let licenseDisabled = true;
let licenseHasScreenshotReporting = false;
@ -110,22 +80,13 @@ export const reportingScreenshotShareProvider = ({
capabilityHasVisualizeScreenshotReporting = true;
}
// If the TZ is set to the default "Browser", it will not be useful for
// server-side export. We need to derive the timezone and pass it as a param
// to the export API.
// TODO: create a helper utility in Reporting. This is repeated in a few places.
const browserTimezone =
uiSettings.get('dateFormat:tz') === 'Browser'
? moment.tz.guess()
: uiSettings.get('dateFormat:tz');
const getShareMenuItems = ({
objectType,
objectId,
sharingData,
isDirty,
onClose,
shareableUrl,
...shareOpts
}: ShareContext) => {
if (!licenseHasScreenshotReporting) {
return [];
@ -143,6 +104,7 @@ export const reportingScreenshotShareProvider = ({
return [];
}
const { sharingData } = (shareOpts as unknown) as { sharingData: ReportingSharingData };
const shareActions = [];
const pngPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pngReportsButtonLabel', {
@ -165,16 +127,11 @@ export const reportingScreenshotShareProvider = ({
<ScreenCapturePanelContent
apiClient={apiClient}
toasts={toasts}
uiSettings={uiSettings}
reportType="png"
objectId={objectId}
requiresSavedState={true}
getJobParams={getPngJobParams({
shareableUrl,
apiClient,
objectType,
browserTimezone,
sharingData,
})}
getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'png')}
isDirty={isDirty}
onClose={onClose}
/>
@ -202,17 +159,12 @@ export const reportingScreenshotShareProvider = ({
<ScreenCapturePanelContent
apiClient={apiClient}
toasts={toasts}
uiSettings={uiSettings}
reportType="printablePdf"
objectId={objectId}
requiresSavedState={true}
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
getJobParams={getPdfJobParams({
shareableUrl,
apiClient,
objectType,
browserTimezone,
sharingData,
})}
getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'pdf')}
isDirty={isDirty}
onClose={onClose}
/>

View file

@ -7,26 +7,56 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { notificationServiceMock } from 'src/core/public/mocks';
import { ReportingPanelContent, Props } from './reporting_panel_content';
import {
httpServiceMock,
notificationServiceMock,
uiSettingsServiceMock,
} from 'src/core/public/mocks';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ReportingPanelContent, ReportingPanelProps as Props } from './reporting_panel_content';
describe('ReportingPanelContent', () => {
const mountComponent = (props: Partial<Props>) =>
const props: Partial<Props> = {
layoutId: 'super_cool_layout_id_X',
};
const jobParams = {
appState: 'very_cool_app_state_X',
objectType: 'noice_object',
title: 'ultimate_title',
};
const toasts = notificationServiceMock.createSetupContract().toasts;
const http = httpServiceMock.createSetupContract();
const uiSettings = uiSettingsServiceMock.createSetupContract();
let apiClient: ReportingAPIClient;
beforeEach(() => {
props.layoutId = 'super_cool_layout_id_X';
uiSettings.get.mockImplementation((key: string) => {
switch (key) {
case 'dateFormat:tz':
return 'Mars';
}
});
apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0-test');
});
const mountComponent = (newProps: Partial<Props>) =>
mountWithIntl(
<ReportingPanelContent
requiresSavedState
// We have unsaved changes
isDirty={true}
isDirty={true} // We have unsaved changes
reportType="test"
layoutId="test"
getJobParams={jest.fn().mockReturnValue({})}
objectId={'my-object-id'}
apiClient={{ getReportingJobPath: () => 'test' } as any}
toasts={notificationServiceMock.createSetupContract().toasts}
objectId="my-object-id"
layoutId={props.layoutId}
getJobParams={() => jobParams}
apiClient={apiClient}
toasts={toasts}
uiSettings={uiSettings}
{...props}
{...newProps}
/>
);
describe('saved state', () => {
it('prevents generating reports when saving is required and we have unsaved changes', () => {
const wrapper = mountComponent({
@ -51,5 +81,20 @@ describe('ReportingPanelContent', () => {
false
);
});
it('changing the layout triggers refreshing the state with the latest job params', () => {
const wrapper = mountComponent({ requiresSavedState: false });
wrapper.update();
expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot(
`"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_cool_app_state_X%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"`
);
jobParams.appState = 'very_NOT_cool_app_state_Y';
wrapper.setProps({ layoutId: 'super_cool_layout_id_Y' }); // update the component internal state
wrapper.update();
expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot(
`"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_NOT_cool_app_state_Y%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"`
);
});
});
});

View file

@ -18,29 +18,30 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { Component, ReactElement } from 'react';
import { ToastsSetup } from 'src/core/public';
import { ToastsSetup, IUiSettingsClient } from 'src/core/public';
import url from 'url';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants';
import { BaseParams } from '../../common/types';
import { ReportingAPIClient } from '../lib/reporting_api_client';
export interface Props {
export interface ReportingPanelProps {
apiClient: ReportingAPIClient;
toasts: ToastsSetup;
uiSettings: IUiSettingsClient;
reportType: string;
/** Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. **/
requiresSavedState: boolean;
layoutId: string | undefined;
requiresSavedState: boolean; // Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator.
layoutId?: string;
objectId?: string;
getJobParams: () => BaseParams;
getJobParams: () => Omit<BaseParams, 'browserTimezone' | 'version'>;
options?: ReactElement<any> | null;
isDirty?: boolean;
onClose?: () => void;
intl: InjectedIntl;
}
export type Props = ReportingPanelProps & { intl: InjectedIntl };
interface State {
isStale: boolean;
absoluteUrl: string;
@ -68,12 +69,12 @@ class ReportingPanelContentUi extends Component<Props, State> {
private getAbsoluteReportGenerationUrl = (props: Props) => {
const relativePath = this.props.apiClient.getReportingJobPath(
props.reportType,
props.getJobParams()
this.props.apiClient.getDecoratedJobParams(this.props.getJobParams())
);
return url.resolve(window.location.href, relativePath);
return url.resolve(window.location.href, relativePath); // FIXME: '(from: string, to: string): string' is deprecated
};
public componentDidUpdate(prevProps: Props, prevState: State) {
public componentDidUpdate(_prevProps: Props, prevState: State) {
if (this.props.layoutId && this.props.layoutId !== prevState.layoutId) {
this.setState({
...prevState,
@ -231,9 +232,12 @@ class ReportingPanelContentUi extends Component<Props, State> {
private createReportingJob = () => {
const { intl } = this.props;
const decoratedJobParams = this.props.apiClient.getDecoratedJobParams(
this.props.getJobParams()
);
return this.props.apiClient
.createReportingJob(this.props.reportType, this.props.getJobParams())
.createReportingJob(this.props.reportType, decoratedJobParams)
.then(() => {
this.props.toasts.addSuccess({
title: intl.formatMessage(

View file

@ -8,25 +8,33 @@
import { mount } from 'enzyme';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n/react';
import { coreMock } from '../../../../../src/core/public/mocks';
import { BaseParams } from '../../common/types';
import { coreMock } from 'src/core/public/mocks';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ScreenCapturePanelContent } from './screen_capture_panel_content';
const getJobParamsDefault: () => BaseParams = () => ({
const { http, uiSettings, ...coreSetup } = coreMock.createSetup();
uiSettings.get.mockImplementation((key: string) => {
switch (key) {
case 'dateFormat:tz':
return 'Mars';
}
});
const apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0');
const getJobParamsDefault = () => ({
objectType: 'test-object-type',
title: 'Test Report Title',
browserTimezone: 'America/New_York',
});
test('ScreenCapturePanelContent renders the default view properly', () => {
const coreSetup = coreMock.createSetup();
const component = mount(
<IntlProvider locale="en">
<ScreenCapturePanelContent
reportType="Analytical App"
requiresSavedState={false}
apiClient={new ReportingAPIClient(coreSetup.http)}
apiClient={apiClient}
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
/>
@ -38,14 +46,14 @@ test('ScreenCapturePanelContent renders the default view properly', () => {
});
test('ScreenCapturePanelContent properly renders a view with "canvas" layout option', () => {
const coreSetup = coreMock.createSetup();
const component = mount(
<IntlProvider locale="en">
<ScreenCapturePanelContent
layoutOption="canvas"
reportType="Analytical App"
requiresSavedState={false}
apiClient={new ReportingAPIClient(coreSetup.http)}
apiClient={apiClient}
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
/>
@ -56,14 +64,14 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt
});
test('ScreenCapturePanelContent properly renders a view with "print" layout option', () => {
const coreSetup = coreMock.createSetup();
const component = mount(
<IntlProvider locale="en">
<ScreenCapturePanelContent
layoutOption="print"
reportType="Analytical App"
requiresSavedState={false}
apiClient={new ReportingAPIClient(coreSetup.http)}
apiClient={apiClient}
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
/>
@ -72,3 +80,22 @@ test('ScreenCapturePanelContent properly renders a view with "print" layout opti
expect(component.find('EuiForm')).toMatchSnapshot();
expect(component.text()).toMatch('Optimize for printing');
});
test('ScreenCapturePanelContent decorated job params are visible in the POST URL', () => {
const component = mount(
<IntlProvider locale="en">
<ScreenCapturePanelContent
reportType="Analytical App"
requiresSavedState={false}
apiClient={apiClient}
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
/>
</IntlProvider>
);
expect(component.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot(
`"http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%2Cversion%3A%277.15.0%27%29"`
);
});

View file

@ -7,24 +7,13 @@
import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { Component } from 'react';
import { ToastsSetup } from 'src/core/public';
import { getDefaultLayoutSelectors } from '../../common';
import { BaseParams, LayoutParams } from '../../common/types';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ReportingPanelContent } from './reporting_panel_content';
import { LayoutParams } from '../../common/types';
import { ReportingPanelContent, ReportingPanelProps } from './reporting_panel_content';
export interface Props {
apiClient: ReportingAPIClient;
toasts: ToastsSetup;
reportType: string;
export interface Props extends ReportingPanelProps {
layoutOption?: 'canvas' | 'print';
objectId?: string;
getJobParams: () => BaseParams;
requiresSavedState: boolean;
isDirty?: boolean;
onClose?: () => void;
}
interface State {
@ -45,16 +34,10 @@ export class ScreenCapturePanelContent extends Component<Props, State> {
public render() {
return (
<ReportingPanelContent
requiresSavedState={this.props.requiresSavedState}
apiClient={this.props.apiClient}
toasts={this.props.toasts}
reportType={this.props.reportType}
{...this.props}
layoutId={this.getLayout().id}
objectId={this.props.objectId}
getJobParams={this.getJobParams}
options={this.renderOptions()}
isDirty={this.props.isDirty}
onClose={this.props.onClose}
/>
);
}
@ -147,17 +130,10 @@ export class ScreenCapturePanelContent extends Component<Props, State> {
return { id: 'preserve_layout', dimensions, selectors };
};
private getJobParams = (): Required<BaseParams> => {
const outerParams = this.props.getJobParams();
let browserTimezone = outerParams.browserTimezone;
if (!browserTimezone) {
browserTimezone = moment.tz.guess();
}
private getJobParams = () => {
return {
...this.props.getJobParams(),
layout: this.getLayout(),
browserTimezone,
};
};
}

View file

@ -23,7 +23,7 @@ type PropsPDF = Pick<PanelPropsScreenCapture, 'getJobParams' | 'layoutOption'> &
* This is not planned to expand, as work is to be done on moving the export-type implementations out of Reporting
* Related Discuss issue: https://github.com/elastic/kibana/issues/101422
*/
export function getSharedComponents(core: CoreSetup) {
export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) {
return {
ReportingPanelPDF(props: PropsPDF) {
return (
@ -31,8 +31,9 @@ export function getSharedComponents(core: CoreSetup) {
layoutOption={props.layoutOption}
requiresSavedState={false}
reportType={PDF_REPORT_TYPE}
apiClient={new ReportingAPIClient(core.http)}
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
{...props}
/>
);

View file

@ -58,6 +58,7 @@ export interface ReportingInternalStart {
}
export class ReportingCore {
private kibanaVersion: string;
private pluginSetupDeps?: ReportingInternalSetup;
private pluginStartDeps?: ReportingInternalStart;
private readonly pluginSetup$ = new Rx.ReplaySubject<boolean>(); // observe async background setupDeps and config each are done
@ -72,6 +73,7 @@ export class ReportingCore {
public getContract: () => ReportingSetup;
constructor(private logger: LevelLogger, context: PluginInitializerContext<ReportingConfigType>) {
this.kibanaVersion = context.env.packageInfo.version;
const syncConfig = context.config.get<ReportingConfigType>();
this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false;
this.executeTask = new ExecuteReportTask(this, syncConfig, this.logger);
@ -84,6 +86,10 @@ export class ReportingCore {
this.executing = new Set();
}
public getKibanaVersion() {
return this.kibanaVersion;
}
/*
* Register setupDeps
*/

View file

@ -59,6 +59,7 @@ test('gets the csv content from job parameters', async () => {
searchSource: {},
objectType: 'search',
title: 'Test Search',
version: '7.13.0',
},
new CancellationToken()
);

View file

@ -12,6 +12,7 @@ import { IScopedSearchClient } from 'src/plugins/data/server';
import { Datatable } from 'src/plugins/expressions/server';
import { ReportingConfig } from '../../..';
import {
cellHasFormulas,
ES_SEARCH_STRATEGY,
FieldFormat,
FieldFormatConfig,
@ -22,7 +23,6 @@ import {
SearchFieldValue,
SearchSourceFields,
tabifyDocs,
cellHasFormulas,
} from '../../../../../../../src/plugins/data/common';
import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server';
import { CancellationToken } from '../../../../common';
@ -68,7 +68,7 @@ export class CsvGenerator {
private csvRowCount = 0;
constructor(
private job: JobParamsCSV,
private job: Omit<JobParamsCSV, 'version'>,
private config: ReportingConfig,
private clients: Clients,
private dependencies: Dependencies,
@ -219,7 +219,6 @@ export class CsvGenerator {
*/
private generateHeader(
columns: string[],
table: Datatable,
builder: MaxSizeStringBuilder,
settings: CsvExportSettings
) {
@ -357,7 +356,7 @@ export class CsvGenerator {
if (first) {
first = false;
this.generateHeader(columns, table, builder, settings);
this.generateHeader(columns, builder, settings);
}
if (table.rows.length < 1) {

View file

@ -11,7 +11,6 @@ import type { BaseParams, BasePayload } from '../../types';
export type RawValue = string | object | null | undefined;
interface BaseParamsCSV {
browserTimezone: string;
searchSource: SearchSourceFields;
columns?: string[];
}

View file

@ -32,7 +32,7 @@ export const runTaskFnFactory: RunTaskFnFactory<ImmediateExecuteFn> = function e
const config = reporting.getConfig();
const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']);
return async function runTask(jobId, immediateJobParams, context, req) {
return async function runTask(_jobId, immediateJobParams, context, req) {
const job = {
objectType: 'immediate-search',
...immediateJobParams,

View file

@ -16,24 +16,16 @@ export const createJobFnFactory: CreateJobFnFactory<
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob(
{ objectType, title, relativeUrl, browserTimezone, layout },
context,
req
) {
return async function createJob(jobParams, _context, req) {
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
validateUrls([relativeUrl]);
validateUrls([jobParams.relativeUrl]);
return {
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(req, logger),
objectType,
title,
relativeUrl,
browserTimezone,
layout,
forceNow: new Date().toISOString(),
...jobParams,
};
};
};

View file

@ -16,24 +16,16 @@ export const createJobFnFactory: CreateJobFnFactory<
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob(
{ title, relativeUrls, browserTimezone, layout, objectType },
context,
req
) {
return async function createJob(jobParams, _context, req) {
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
validateUrls(relativeUrls);
validateUrls(jobParams.relativeUrls);
return {
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(req, logger),
browserTimezone,
forceNow: new Date().toISOString(),
layout,
relativeUrls,
title,
objectType,
...jobParams,
};
};
};

View file

@ -0,0 +1,20 @@
/*
* 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 { UNVERSIONED_VERSION } from '../../common/constants';
import type { BaseParams } from '../../common/types';
import type { LevelLogger } from './';
export function checkParamsVersion(jobParams: BaseParams, logger: LevelLogger) {
if (jobParams.version) {
logger.debug(`Using reporting job params v${jobParams.version}`);
return jobParams.version;
}
logger.warning(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`);
return UNVERSIONED_VERSION;
}

View file

@ -14,17 +14,28 @@ import {
createMockLevelLogger,
createMockReportingCore,
} from '../test_helpers';
import { BasePayload, ReportingRequestHandlerContext } from '../types';
import { ReportingRequestHandlerContext } from '../types';
import { ExportTypesRegistry, ReportingStore } from './';
import { enqueueJobFactory } from './enqueue_job';
import { Report } from './store';
import { TaskRunResult } from './tasks';
describe('Enqueue Job', () => {
const logger = createMockLevelLogger();
let mockReporting: ReportingCore;
let mockExportTypesRegistry: ExportTypesRegistry;
const mockBaseParams = {
browserTimezone: 'UTC',
headers: 'cool_encrypted_headers',
objectType: 'cool_object_type',
title: 'cool_title',
version: 'unknown' as any,
};
beforeEach(() => {
mockBaseParams.version = '7.15.0-test';
});
beforeAll(async () => {
mockExportTypesRegistry = new ExportTypesRegistry();
mockExportTypesRegistry.register({
@ -34,10 +45,8 @@ describe('Enqueue Job', () => {
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
validLicenses: ['turquoise'],
createJobFnFactory: () => async () =>
(({ createJobTest: { test1: 'yes' } } as unknown) as BasePayload),
runTaskFnFactory: () => async () =>
(({ runParamsTest: { test2: 'yes' } } as unknown) as TaskRunResult),
createJobFnFactory: () => async () => mockBaseParams,
runTaskFnFactory: jest.fn(),
});
mockReporting = await createMockReportingCore(createMockConfigSchema());
mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry;
@ -66,26 +75,59 @@ describe('Enqueue Job', () => {
const enqueueJob = enqueueJobFactory(mockReporting, logger);
const report = await enqueueJob(
'printablePdf',
{
objectType: 'visualization',
title: 'cool-viz',
},
mockBaseParams,
false,
({} as unknown) as ReportingRequestHandlerContext,
({} as unknown) as KibanaRequest
);
expect(report).toMatchObject({
_id: expect.any(String),
_index: '.reporting-foo-index-234',
attempts: 0,
created_by: false,
created_at: expect.any(String),
jobtype: 'printable_pdf',
meta: { objectType: 'visualization' },
output: null,
payload: { createJobTest: { test1: 'yes' } },
status: 'pending',
});
const { _id, created_at: _created_at, ...snapObj } = report;
expect(snapObj).toMatchInlineSnapshot(`
Object {
"_index": ".reporting-foo-index-234",
"_primary_term": undefined,
"_seq_no": undefined,
"attempts": 0,
"browser_type": undefined,
"completed_at": undefined,
"created_by": false,
"jobtype": "printable_pdf",
"kibana_id": undefined,
"kibana_name": undefined,
"max_attempts": undefined,
"meta": Object {
"layout": undefined,
"objectType": "cool_object_type",
},
"migration_version": "7.14.0",
"output": null,
"payload": Object {
"browserTimezone": "UTC",
"headers": "cool_encrypted_headers",
"objectType": "cool_object_type",
"title": "cool_title",
"version": "7.15.0-test",
},
"process_expiration": undefined,
"started_at": undefined,
"status": "pending",
"timeout": undefined,
}
`);
});
it('provides a default kibana version field for older POST URLs', async () => {
const enqueueJob = enqueueJobFactory(mockReporting, logger);
mockBaseParams.version = undefined;
const report = await enqueueJob(
'printablePdf',
mockBaseParams,
false,
({} as unknown) as ReportingRequestHandlerContext,
({} as unknown) as KibanaRequest
);
const { _id, created_at: _created_at, ...snapObj } = report;
expect(snapObj.payload.version).toBe('7.14.0');
});
});

View file

@ -7,10 +7,10 @@
import { KibanaRequest } from 'src/core/server';
import { ReportingCore } from '../';
import { BaseParams, ReportingUser } from '../types';
import { LevelLogger } from './';
import { Report } from './store';
import type { ReportingRequestHandlerContext } from '../types';
import { BaseParams, ReportingUser } from '../types';
import { checkParamsVersion, LevelLogger } from './';
import { Report } from './store';
export type EnqueueJobFn = (
exportTypeId: string,
@ -47,6 +47,7 @@ export function enqueueJobFactory(
reporting.getStore(),
]);
jobParams.version = checkParamsVersion(jobParams, logger);
const job = await createJob!(jobParams, context, request);
// 1. Add the report to ReportingStore to show as pending

View file

@ -6,6 +6,7 @@
*/
export { checkLicense } from './check_license';
export { checkParamsVersion } from './check_params_version';
export { cryptoFactory } from './crypto';
export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry';
export { LevelLogger } from './level_logger';

View file

@ -15,7 +15,13 @@ describe('Class Report', () => {
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'cool report' },
payload: {
headers: 'payload_test_field',
objectType: 'testOt',
title: 'cool report',
version: '7.14.0',
browserTimezone: 'UTC',
},
meta: { objectType: 'test' },
timeout: 30000,
});
@ -64,7 +70,13 @@ describe('Class Report', () => {
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'hot report' },
payload: {
headers: 'payload_test_field',
objectType: 'testOt',
title: 'hot report',
version: '7.14.0',
browserTimezone: 'UTC',
},
meta: { objectType: 'stange' },
timeout: 30000,
});

View file

@ -255,6 +255,7 @@ describe('ReportingStore', () => {
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'ABC',
version: '7.14.0',
},
timeout: 30000,
});
@ -285,6 +286,7 @@ describe('ReportingStore', () => {
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'BCD',
version: '7.14.0',
},
timeout: 30000,
});
@ -315,6 +317,7 @@ describe('ReportingStore', () => {
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'CDE',
version: '7.14.0',
},
timeout: 30000,
});
@ -345,6 +348,7 @@ describe('ReportingStore', () => {
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'utc',
version: '7.14.0',
},
timeout: 30000,
});
@ -390,6 +394,7 @@ describe('ReportingStore', () => {
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'utc',
version: '7.14.0',
},
timeout: 30000,
});

View file

@ -55,13 +55,14 @@ export function registerGenerateCsvFromSavedObjectImmediate(
searchSource: schema.object({}, { unknowns: 'allow' }),
browserTimezone: schema.string({ defaultValue: 'UTC' }),
title: schema.string(),
version: schema.maybe(schema.string()),
}),
},
options: {
tags: kibanaAccessControlTags,
},
},
userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => {
userHandler(async (_user, context, req: CsvFromSavedObjectRequest, res) => {
const logger = parentLogger.clone(['csv_searchsource_immediate']);
const runTaskFn = runTaskFnFactory(reporting, logger);

View file

@ -95,7 +95,7 @@ export function registerGenerateFromJobParams(
path: `${BASE_GENERATE}/{p*}`,
validate: false,
},
(context, req, res) => {
(_context, _req, res) => {
return res.customError({ statusCode: 405, body: 'GET is not allowed' });
}
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import rison from 'rison-node';
import { UnwrapPromise } from '@kbn/utility-types';
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import { of } from 'rxjs';
@ -129,7 +130,7 @@ describe('POST /api/reporting/generate', () => {
await supertest(httpSetup.server.listener)
.post('/api/reporting/generate/TonyHawksProSkater2')
.send({ jobParams: `abc` })
.send({ jobParams: rison.encode({ title: `abc` }) })
.expect(400)
.then(({ body }) =>
expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"')
@ -145,7 +146,7 @@ describe('POST /api/reporting/generate', () => {
await supertest(httpSetup.server.listener)
.post('/api/reporting/generate/printablePdf')
.send({ jobParams: `abc` })
.send({ jobParams: rison.encode({ title: `abc` }) })
.expect(500);
});
@ -157,7 +158,7 @@ describe('POST /api/reporting/generate', () => {
await supertest(httpSetup.server.listener)
.post('/api/reporting/generate/printablePdf')
.send({ jobParams: `abc` })
.send({ jobParams: rison.encode({ title: `abc` }) })
.expect(200)
.then(({ body }) => {
expect(body).toMatchObject({

View file

@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) {
},
browserTimezone: 'UTC',
title: 'testfooyu78yt90-',
version: '7.13.0',
} as any
)) as supertest.Response;
expect(res.status).to.eql(403);
@ -52,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) {
},
browserTimezone: 'UTC',
title: 'testfooyu78yt90-',
version: '7.13.0',
} as any
)) as supertest.Response;
expect(res.status).to.eql(200);
@ -69,6 +71,7 @@ export default function ({ getService }: FtrProviderContext) {
layout: { id: 'preserve' },
relativeUrls: ['/fooyou'],
objectType: 'dashboard',
version: '7.14.0',
}
);
expect(res.status).to.eql(403);
@ -84,6 +87,7 @@ export default function ({ getService }: FtrProviderContext) {
layout: { id: 'preserve' },
relativeUrls: ['/fooyou'],
objectType: 'dashboard',
version: '7.14.0',
}
);
expect(res.status).to.eql(200);
@ -101,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) {
layout: { id: 'preserve' },
relativeUrls: ['/fooyou'],
objectType: 'visualization',
version: '7.14.0',
}
);
expect(res.status).to.eql(403);
@ -116,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) {
layout: { id: 'preserve' },
relativeUrls: ['/fooyou'],
objectType: 'visualization',
version: '7.14.0',
}
);
expect(res.status).to.eql(200);
@ -133,6 +139,7 @@ export default function ({ getService }: FtrProviderContext) {
layout: { id: 'preserve' },
relativeUrls: ['/fooyou'],
objectType: 'canvas',
version: '7.14.0',
}
);
expect(res.status).to.eql(403);
@ -148,6 +155,7 @@ export default function ({ getService }: FtrProviderContext) {
layout: { id: 'preserve' },
relativeUrls: ['/fooyou'],
objectType: 'canvas',
version: '7.14.0',
}
);
expect(res.status).to.eql(200);
@ -164,6 +172,7 @@ export default function ({ getService }: FtrProviderContext) {
searchSource: {},
objectType: 'search',
title: 'test disallowed',
version: '7.14.0',
}
);
expect(res.status).to.eql(403);
@ -183,6 +192,7 @@ export default function ({ getService }: FtrProviderContext) {
index: '5193f870-d861-11e9-a311-0fa548c5f953',
} as any,
columns: [],
version: '7.13.0',
}
);
expect(res.status).to.eql(200);