From ea2d73642ad745886c6ed523f2b56cbc1065dd0b Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 27 Feb 2020 10:39:36 -0800 Subject: [PATCH 01/23] Move over to new plugin space, working implementation --- x-pack/legacy/plugins/reporting/index.ts | 2 - x-pack/plugins/reporting/common/poller.ts | 96 ++++ x-pack/plugins/reporting/constants.ts | 15 + x-pack/plugins/reporting/index.d.ts | 12 + x-pack/plugins/reporting/kibana.json | 2 +- .../public/components/report_error_button.tsx | 115 +++++ .../report_info_button.test.mocks.ts | 8 + .../components/report_info_button.test.tsx | 56 ++ .../public/components/report_info_button.tsx | 288 +++++++++++ .../public/components/report_listing.test.tsx | 77 +++ .../public/components/report_listing.tsx | 482 ++++++++++++++++++ .../lib/job_completion_notifications.ts | 36 ++ .../reporting/public/lib/job_queue_client.ts | 103 ++++ x-pack/plugins/reporting/public/plugin.tsx | 69 ++- 14 files changed, 1355 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/reporting/common/poller.ts create mode 100644 x-pack/plugins/reporting/public/components/report_error_button.tsx create mode 100644 x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts create mode 100644 x-pack/plugins/reporting/public/components/report_info_button.test.tsx create mode 100644 x-pack/plugins/reporting/public/components/report_info_button.tsx create mode 100644 x-pack/plugins/reporting/public/components/report_listing.test.tsx create mode 100644 x-pack/plugins/reporting/public/components/report_listing.tsx create mode 100644 x-pack/plugins/reporting/public/lib/job_completion_notifications.ts create mode 100644 x-pack/plugins/reporting/public/lib/job_queue_client.ts diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 9ce4e807f8ef8..b648bd276311d 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -30,8 +30,6 @@ export const reporting = (kibana: any) => { 'plugins/reporting/share_context_menu/register_reporting', ], embeddableActions: ['plugins/reporting/panel_actions/get_csv_panel_action'], - home: ['plugins/reporting/register_feature'], - managementSections: ['plugins/reporting/views/management'], injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { const config = server.config(); return { diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts new file mode 100644 index 0000000000000..919d7273062a8 --- /dev/null +++ b/x-pack/plugins/reporting/common/poller.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { PollerOptions } from '..'; + +// @TODO Maybe move to observables someday +export class Poller { + private readonly functionToPoll: () => Promise; + private readonly successFunction: (...args: any) => any; + private readonly errorFunction: (error: Error) => any; + private _isRunning: boolean; + private _timeoutId: NodeJS.Timeout | null; + private pollFrequencyInMillis: number; + private trailing: boolean; + private continuePollingOnError: boolean; + private pollFrequencyErrorMultiplier: number; + + constructor(options: PollerOptions) { + this.functionToPoll = options.functionToPoll; // Must return a Promise + this.successFunction = options.successFunction || _.noop; + this.errorFunction = options.errorFunction || _.noop; + this.pollFrequencyInMillis = options.pollFrequencyInMillis; + this.trailing = options.trailing || false; + this.continuePollingOnError = options.continuePollingOnError || false; + this.pollFrequencyErrorMultiplier = options.pollFrequencyErrorMultiplier || 1; + + this._timeoutId = null; + this._isRunning = false; + } + + getPollFrequency() { + return this.pollFrequencyInMillis; + } + + _poll() { + return this.functionToPoll() + .then(this.successFunction) + .then(() => { + if (!this._isRunning) { + return; + } + + this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); + }) + .catch(e => { + this.errorFunction(e); + if (!this._isRunning) { + return; + } + + if (this.continuePollingOnError) { + this._timeoutId = setTimeout( + this._poll.bind(this), + this.pollFrequencyInMillis * this.pollFrequencyErrorMultiplier + ); + } else { + this.stop(); + } + }); + } + + start() { + if (this._isRunning) { + return; + } + + this._isRunning = true; + if (this.trailing) { + this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); + } else { + this._poll(); + } + } + + stop() { + if (!this._isRunning) { + return; + } + + this._isRunning = false; + + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } + + this._timeoutId = null; + } + + isRunning() { + return this._isRunning; + } +} diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index fe5673a0b74b5..363cfd6b64b7a 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -16,6 +16,21 @@ export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { export const API_BASE_URL = '/api/reporting/jobs'; export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporting'; +export const API_LIST_URL = '/api/reporting/jobs'; export const JOB_STATUS_FAILED = 'failed'; export const JOB_STATUS_COMPLETED = 'completed'; + +export enum JobStatuses { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', +} + +export const PDF_JOB_TYPE = 'printable_pdf'; +export const PNG_JOB_TYPE = 'PNG'; +export const CSV_JOB_TYPE = 'csv'; +export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; +export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 9559de4a5bb03..cd2def9bf10dc 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -13,6 +13,8 @@ import { NotificationsStart, } from '../../../src/core/public'; +export { ToastsSetup, HttpSetup } from 'src/core/public'; + export type JobId = string; export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; @@ -57,3 +59,13 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; + +export interface PollerOptions { + functionToPoll: () => Promise; + pollFrequencyInMillis: number; + trailing?: boolean; + continuePollingOnError?: boolean; + pollFrequencyErrorMultiplier?: number; + successFunction?: (...args: any) => any; + errorFunction?: (error: Error) => any; +} diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 50f552b0d9fb0..2be3f51a09781 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,7 +2,7 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [], + "requiredPlugins": ["home", "management", "licensing"], "server": false, "ui": true } diff --git a/x-pack/plugins/reporting/public/components/report_error_button.tsx b/x-pack/plugins/reporting/public/components/report_error_button.tsx new file mode 100644 index 0000000000000..b33ca2cb81436 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_error_button.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import React, { Component } from 'react'; +import { JobContent, JobQueueClient } from '../lib/job_queue_client'; + +interface Props { + jobId: string; + intl: InjectedIntl; + jobQueueClient: JobQueueClient; +} + +interface State { + isLoading: boolean; + isPopoverOpen: boolean; + calloutTitle: string; + error?: string; +} + +class ReportErrorButtonUi extends Component { + private mounted?: boolean; + + constructor(props: Props) { + super(props); + + this.state = { + isLoading: false, + isPopoverOpen: false, + calloutTitle: props.intl.formatMessage({ + id: 'xpack.reporting.errorButton.unableToGenerateReportTitle', + defaultMessage: 'Unable to generate report', + }), + }; + } + + public render() { + const button = ( + + ); + + return ( + + +

{this.state.error}

+
+
+ ); + } + + public componentWillUnmount() { + this.mounted = false; + } + + public componentDidMount() { + this.mounted = true; + } + + private togglePopover = () => { + this.setState(prevState => { + return { isPopoverOpen: !prevState.isPopoverOpen }; + }); + + if (!this.state.error) { + this.loadError(); + } + }; + + private closePopover = () => { + this.setState({ isPopoverOpen: false }); + }; + + private loadError = async () => { + this.setState({ isLoading: true }); + try { + const reportContent: JobContent = await this.props.jobQueueClient.getContent( + this.props.jobId + ); + if (this.mounted) { + this.setState({ isLoading: false, error: reportContent.content }); + } + } catch (kfetchError) { + if (this.mounted) { + this.setState({ + isLoading: false, + calloutTitle: this.props.intl.formatMessage({ + id: 'xpack.reporting.errorButton.unableToFetchReportContentTitle', + defaultMessage: 'Unable to fetch report content', + }), + error: kfetchError.message, + }); + } + } + }; +} + +export const ReportErrorButton = injectI18n(ReportErrorButtonUi); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts b/x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts new file mode 100644 index 0000000000000..9dd7cbb5fc567 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockJobQueueClient = { list: jest.fn(), total: jest.fn(), getInfo: jest.fn() }; +jest.mock('../lib/job_queue_client', () => ({ jobQueueClient: mockJobQueueClient })); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx new file mode 100644 index 0000000000000..3b9c2a8485423 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockJobQueueClient } from './report_info_button.test.mocks'; + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReportInfoButton } from './report_info_button'; + +describe('ReportInfoButton', () => { + beforeEach(() => { + mockJobQueueClient.getInfo = jest.fn(() => ({ + payload: { title: 'Test Job' }, + })); + }); + + it('handles button click flyout on click', () => { + const wrapper = mountWithIntl(); + const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); + expect(input).toMatchSnapshot(); + }); + + it('opens flyout with info', () => { + const wrapper = mountWithIntl(); + const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); + + input.simulate('click'); + + const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); + expect(flyout).toMatchSnapshot(); + + expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); + expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-456'); + }); + + it('opens flyout with fetch error info', () => { + // simulate fetch failure + mockJobQueueClient.getInfo = jest.fn(() => { + throw new Error('Could not fetch the job info'); + }); + + const wrapper = mountWithIntl(); + const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); + + input.simulate('click'); + + const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); + expect(flyout).toMatchSnapshot(); + + expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); + expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-789'); + }); +}); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx new file mode 100644 index 0000000000000..9531d4ce4ed8f --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiDescriptionList, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { Component, Fragment } from 'react'; +import { get } from 'lodash'; +import { USES_HEADLESS_JOB_TYPES } from '../../constants'; +import { JobInfo, JobQueueClient } from '../lib/job_queue_client'; + +interface Props { + jobId: string; + jobQueueClient: JobQueueClient; +} + +interface State { + isLoading: boolean; + isFlyoutVisible: boolean; + calloutTitle: string; + info: JobInfo | null; + error: Error | null; +} + +const NA = 'n/a'; +const UNKNOWN = 'unknown'; + +const getDimensions = (info: JobInfo): string => { + const defaultDimensions = { width: null, height: null }; + const { width, height } = get(info, 'payload.layout.dimensions', defaultDimensions); + if (width && height) { + return `Width: ${width} x Height: ${height}`; + } + return NA; +}; + +export class ReportInfoButton extends Component { + private mounted?: boolean; + + constructor(props: Props) { + super(props); + + this.state = { + isLoading: false, + isFlyoutVisible: false, + calloutTitle: 'Job Info', + info: null, + error: null, + }; + + this.closeFlyout = this.closeFlyout.bind(this); + this.showFlyout = this.showFlyout.bind(this); + } + + public renderInfo() { + const { info, error: err } = this.state; + if (err) { + return err.message; + } + if (!info) { + return null; + } + + const jobType = info.jobtype || NA; + + interface JobInfo { + title: string; + description: string; + } + + interface JobInfoMap { + [thing: string]: JobInfo[]; + } + + const attempts = info.attempts ? info.attempts.toString() : NA; + const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; + const priority = info.priority ? info.priority.toString() : NA; + const timeout = info.timeout ? info.timeout.toString() : NA; + + const jobInfoParts: JobInfoMap = { + datetimes: [ + { + title: 'Created By', + description: info.created_by || NA, + }, + { + title: 'Created At', + description: info.created_at || NA, + }, + { + title: 'Started At', + description: info.started_at || NA, + }, + { + title: 'Completed At', + description: info.completed_at || NA, + }, + { + title: 'Processed By', + description: + info.kibana_name && info.kibana_id + ? `${info.kibana_name} (${info.kibana_id})` + : UNKNOWN, + }, + { + title: 'Browser Timezone', + description: get(info, 'payload.browserTimezone') || NA, + }, + ], + payload: [ + { + title: 'Title', + description: get(info, 'payload.title') || NA, + }, + { + title: 'Type', + description: get(info, 'payload.type') || NA, + }, + { + title: 'Layout', + description: get(info, 'meta.layout') || NA, + }, + { + title: 'Dimensions', + description: getDimensions(info), + }, + { + title: 'Job Type', + description: jobType, + }, + { + title: 'Content Type', + description: get(info, 'output.content_type') || NA, + }, + { + title: 'Size in Bytes', + description: get(info, 'output.size') || NA, + }, + ], + status: [ + { + title: 'Attempts', + description: attempts, + }, + { + title: 'Max Attempts', + description: maxAttempts, + }, + { + title: 'Priority', + description: priority, + }, + { + title: 'Timeout', + description: timeout, + }, + { + title: 'Status', + description: info.status || NA, + }, + { + title: 'Browser Type', + description: USES_HEADLESS_JOB_TYPES.includes(jobType) + ? info.browser_type || UNKNOWN + : NA, + }, + ], + }; + + return ( + + + + + + + + ); + } + + public componentWillUnmount() { + this.mounted = false; + } + + public componentDidMount() { + this.mounted = true; + } + + public render() { + let flyout; + + if (this.state.isFlyoutVisible) { + flyout = ( + + + + +

{this.state.calloutTitle}

+
+
+ + {this.renderInfo()} + +
+
+ ); + } + + return ( + + + {flyout} + + ); + } + + private loadInfo = async () => { + this.setState({ isLoading: true }); + try { + const info: JobInfo = await this.props.jobQueueClient.getInfo(this.props.jobId); + if (this.mounted) { + this.setState({ isLoading: false, info }); + } + } catch (kfetchError) { + if (this.mounted) { + this.setState({ + isLoading: false, + calloutTitle: 'Unable to fetch report info', + info: null, + error: kfetchError, + }); + } + } + }; + + private closeFlyout = () => { + this.setState({ + isFlyoutVisible: false, + info: null, // force re-read for next click + }); + }; + + private showFlyout = () => { + this.setState({ isFlyoutVisible: true }); + + if (!this.state.info) { + this.loadInfo(); + } + }; +} diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx new file mode 100644 index 0000000000000..d78eb5c409c1f --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface JobData { + _index: string; + _id: string; + _source: { + browser_type: string; + created_at: string; + jobtype: string; + created_by: string; + payload: { + type: string; + title: string; + }; + kibana_name?: string; // undefined if job is pending (not yet claimed by an instance) + kibana_id?: string; // undefined if job is pending (not yet claimed by an instance) + output?: { content_type: string; size: number }; // undefined if job is incomplete + completed_at?: string; // undefined if job is incomplete + }; +} + +jest.mock('ui/chrome', () => ({ + getInjected() { + return { + jobsRefresh: { + interval: 10, + intervalErrorMultiplier: 2, + }, + }; + }, +})); + +jest.mock('ui/kfetch', () => ({ + kfetch: ({ pathname }: { pathname: string }): Promise => { + if (pathname === '/api/reporting/jobs/list') { + return Promise.resolve([ + { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore + ]); + } + + // query for jobs count + return Promise.resolve(18); + }, +})); + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReportListing } from './report_listing'; + +describe('ReportListing', () => { + it('Report job listing with some items', () => { + const wrapper = mountWithIntl( + + ); + wrapper.update(); + const input = wrapper.find('[data-test-subj="reportJobListing"]'); + expect(input).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx new file mode 100644 index 0000000000000..2618820f99de1 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -0,0 +1,482 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import moment from 'moment'; +import { get } from 'lodash'; + +import React, { Component } from 'react'; +import { + EuiBasicTable, + EuiButtonIcon, + EuiPageContent, + EuiSpacer, + EuiText, + EuiTextColor, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; + +import { ToastsSetup } from '../../'; +import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../../../licensing/public'; +import { Poller } from '../../common/poller'; +import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; +import { JobQueueClient, JobQueueEntry } from '../lib/job_queue_client'; +import { ReportErrorButton } from './report_error_button'; +import { ReportInfoButton } from './report_info_button'; + +interface Job { + id: string; + type: string; + object_type: string; + object_title: string; + created_by?: string; + created_at: string; + started_at?: string; + completed_at?: string; + status: string; + statusLabel: string; + max_size_reached: boolean; + attempts: number; + max_attempts: number; + csv_contains_formulas: boolean; +} + +interface Props { + redirect: (url: string) => void; + intl: InjectedIntl; + // @TODO: Get right types here + toasts: ToastsSetup; + jobQueueClient: JobQueueClient; + license$: LicensingPluginSetup['license$']; +} + +interface State { + page: number; + total: number; + jobs: Job[]; + isLoading: boolean; + showLinks: boolean; + enableLinks: boolean; + badLicenseMessage: string; +} + +const jobStatusLabelsMap = new Map([ + [ + JobStatuses.PENDING, + i18n.translate('xpack.reporting.jobStatuses.pendingText', { + defaultMessage: 'Pending', + }), + ], + [ + JobStatuses.PROCESSING, + i18n.translate('xpack.reporting.jobStatuses.processingText', { + defaultMessage: 'Processing', + }), + ], + [ + JobStatuses.COMPLETED, + i18n.translate('xpack.reporting.jobStatuses.completedText', { + defaultMessage: 'Completed', + }), + ], + [ + JobStatuses.FAILED, + i18n.translate('xpack.reporting.jobStatuses.failedText', { + defaultMessage: 'Failed', + }), + ], + [ + JobStatuses.CANCELLED, + i18n.translate('xpack.reporting.jobStatuses.cancelledText', { + defaultMessage: 'Cancelled', + }), + ], +]); + +class ReportListingUi extends Component { + private mounted?: boolean; + private poller?: any; + private isInitialJobsFetch: boolean; + + constructor(props: Props) { + super(props); + + this.state = { + page: 0, + total: 0, + jobs: [], + isLoading: false, + showLinks: false, + enableLinks: false, + badLicenseMessage: '', + }; + + this.isInitialJobsFetch = true; + + this.props.license$.subscribe(license => { + const { state, message } = license.check('reporting', 'basic'); + const enableLinks = state === LICENSE_CHECK_STATE.Valid; + const showLinks = enableLinks && license.getFeature('csv').isAvailable; + + this.setState({ + enableLinks, + showLinks, + badLicenseMessage: message || '', + }); + }); + } + + public render() { + return ( + + +

+ +

+
+ +

+ +

+
+ + {this.renderTable()} +
+ ); + } + + public componentWillUnmount() { + this.mounted = false; + this.poller.stop(); + } + + public componentDidMount() { + this.mounted = true; + this.poller = new Poller({ + functionToPoll: () => { + return this.fetchJobs(); + }, + pollFrequencyInMillis: + JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.interval, + trailing: false, + continuePollingOnError: true, + pollFrequencyErrorMultiplier: + JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier, + }); + this.poller.start(); + } + + private renderTable() { + const { intl } = this.props; + + const tableColumns = [ + { + field: 'object_title', + name: intl.formatMessage({ + id: 'xpack.reporting.listing.tableColumns.reportTitle', + defaultMessage: 'Report', + }), + render: (objectTitle: string, record: Job) => { + return ( +
+
{objectTitle}
+ + {record.object_type} + +
+ ); + }, + }, + { + field: 'created_at', + name: intl.formatMessage({ + id: 'xpack.reporting.listing.tableColumns.createdAtTitle', + defaultMessage: 'Created at', + }), + render: (createdAt: string, record: Job) => { + if (record.created_by) { + return ( +
+
{this.formatDate(createdAt)}
+ {record.created_by} +
+ ); + } + return this.formatDate(createdAt); + }, + }, + { + field: 'status', + name: intl.formatMessage({ + id: 'xpack.reporting.listing.tableColumns.statusTitle', + defaultMessage: 'Status', + }), + render: (status: string, record: Job) => { + if (status === 'pending') { + return ( +
+ +
+ ); + } + + let maxSizeReached; + if (record.max_size_reached) { + maxSizeReached = ( + + + + ); + } + + let statusTimestamp; + if (status === JobStatuses.PROCESSING && record.started_at) { + statusTimestamp = this.formatDate(record.started_at); + } else if ( + record.completed_at && + (status === JobStatuses.COMPLETED || status === JobStatuses.FAILED) + ) { + statusTimestamp = this.formatDate(record.completed_at); + } + + let statusLabel = jobStatusLabelsMap.get(status as JobStatuses) || status; + + if (status === JobStatuses.PROCESSING) { + statusLabel = statusLabel + ` (attempt ${record.attempts} of ${record.max_attempts})`; + } + + if (statusTimestamp) { + return ( +
+ {statusTimestamp}, + }} + /> + {maxSizeReached} +
+ ); + } + + // unknown status + return ( +
+ {statusLabel} + {maxSizeReached} +
+ ); + }, + }, + { + name: intl.formatMessage({ + id: 'xpack.reporting.listing.tableColumns.actionsTitle', + defaultMessage: 'Actions', + }), + actions: [ + { + render: (record: Job) => { + return ( +
+ {this.renderDownloadButton(record)} + {this.renderReportErrorButton(record)} + {this.renderInfoButton(record)} +
+ ); + }, + }, + ], + }, + ]; + + const pagination = { + pageIndex: this.state.page, + pageSize: 10, + totalItemCount: this.state.total, + hidePerPageOptions: true, + }; + + return ( + + ); + } + + private renderDownloadButton = (record: Job) => { + if (record.status !== JobStatuses.COMPLETED) { + return; + } + + const { intl } = this.props; + const button = ( + this.props.jobQueueClient.downloadReport(record.id)} + iconType="importAction" + aria-label={intl.formatMessage({ + id: 'xpack.reporting.listing.table.downloadReportAriaLabel', + defaultMessage: 'Download report', + })} + /> + ); + + if (record.csv_contains_formulas) { + return ( + + {button} + + ); + } + + if (record.max_size_reached) { + return ( + + {button} + + ); + } + + return button; + }; + + private renderReportErrorButton = (record: Job) => { + if (record.status !== JobStatuses.FAILED) { + return; + } + + return ; + }; + + private renderInfoButton = (record: Job) => { + return ; + }; + + private onTableChange = ({ page }: { page: { index: number } }) => { + const { index: pageIndex } = page; + this.setState(() => ({ page: pageIndex }), this.fetchJobs); + }; + + private fetchJobs = async () => { + // avoid page flicker when poller is updating table - only display loading screen on first load + if (this.isInitialJobsFetch) { + this.setState(() => ({ isLoading: true })); + } + + let jobs: JobQueueEntry[]; + let total: number; + try { + jobs = await this.props.jobQueueClient.list(this.state.page); + total = await this.props.jobQueueClient.total(); + this.isInitialJobsFetch = false; + } catch (fetchError) { + if (!this.licenseAllowsToShowThisPage()) { + this.props.toasts.addDanger(this.state.badLicenseMessage); + this.props.redirect('kibana#/management'); + return; + } + + if (fetchError.message === 'Failed to fetch') { + this.props.toasts.addDanger( + fetchError.message || + this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.requestFailedErrorMessage', + defaultMessage: 'Request failed', + }) + ); + } + if (this.mounted) { + this.setState(() => ({ isLoading: false, jobs: [], total: 0 })); + } + return; + } + + if (this.mounted) { + this.setState(() => ({ + isLoading: false, + total, + jobs: jobs.map( + (job: JobQueueEntry): Job => { + const { _source: source } = job; + return { + id: job._id, + type: source.jobtype, + object_type: source.payload.objectType, + object_title: source.payload.title, + created_by: source.created_by, + created_at: source.created_at, + started_at: source.started_at, + completed_at: source.completed_at, + status: source.status, + statusLabel: jobStatusLabelsMap.get(source.status as JobStatuses) || source.status, + max_size_reached: source.output ? source.output.max_size_reached : false, + attempts: source.attempts, + max_attempts: source.max_attempts, + csv_contains_formulas: get(source, 'output.csv_contains_formulas'), + }; + } + ), + })); + } + }; + + private licenseAllowsToShowThisPage = () => { + return this.state.showLinks && this.state.enableLinks; + }; + + private formatDate(timestamp: string) { + try { + return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); + } catch (error) { + // ignore parse error and display unformatted value + return timestamp; + } + } +} + +export const ReportListing = injectI18n(ReportListingUi); diff --git a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts new file mode 100644 index 0000000000000..99f3773856325 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../constants'; + +type jobId = string; + +const set = (jobs: any) => { + sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs)); +}; + +const getAll = () => { + const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); + return sessionValue ? JSON.parse(sessionValue) : []; +}; + +export const add = (jobId: jobId) => { + const jobs = getAll(); + jobs.push(jobId); + set(jobs); +}; + +export const remove = (jobId: jobId) => { + const jobs = getAll(); + const index = jobs.indexOf(jobId); + + if (!index) { + throw new Error('Unable to find job to remove it'); + } + + jobs.splice(index, 1); + set(jobs); +}; diff --git a/x-pack/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/plugins/reporting/public/lib/job_queue_client.ts new file mode 100644 index 0000000000000..2279623375e3a --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/job_queue_client.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from '../../'; +import { API_LIST_URL, API_BASE_URL } from '../../constants'; + +export interface JobQueueEntry { + _id: string; + _source: any; +} + +export interface JobContent { + content: string; + content_type: boolean; +} + +export interface JobInfo { + kibana_name: string; + kibana_id: string; + browser_type: string; + created_at: string; + priority: number; + jobtype: string; + created_by: string; + timeout: number; + output: { + content_type: string; + size: number; + }; + process_expiration: string; + completed_at: string; + payload: { + layout: { id: string; dimensions: { width: number; height: number } }; + objects: Array<{ relativeUrl: string }>; + type: string; + title: string; + forceNow: string; + browserTimezone: string; + }; + meta: { + layout: string; + objectType: string; + }; + max_attempts: number; + started_at: string; + attempts: number; + status: string; +} + +export class JobQueueClient { + private http: HttpSetup; + + constructor(http: HttpSetup) { + this.http = http; + } + + public getReportURL(jobId: string) { + const apiBaseUrl = this.http.basePath.prepend(API_BASE_URL); + const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`; + + return downloadLink; + } + + public downloadReport(jobId: string) { + const location = this.getReportURL(jobId); + + window.open(location); + } + + public list = (page = 0, jobIds: string[] = []): Promise => { + const query = { page } as any; + if (jobIds.length > 0) { + // Only getting the first 10, to prevent URL overflows + query.ids = jobIds.slice(0, 10).join(','); + } + + return this.http.get(`${API_LIST_URL}/list`, { + query, + asSystemRequest: true, + }); + }; + + public total(): Promise { + return this.http.get(`${API_LIST_URL}/count`, { + asSystemRequest: true, + }); + } + + public getContent(jobId: string): Promise { + return this.http.get(`${API_LIST_URL}/output/${jobId}`, { + asSystemRequest: true, + }); + } + + public getInfo(jobId: string): Promise { + return this.http.get(`${API_LIST_URL}/info/${jobId}`, { + asSystemRequest: true, + }); + } +} diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 8786d9748d4a5..fea8750ea7f23 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -5,8 +5,17 @@ */ import * as Rx from 'rxjs'; +import React from 'react'; +import ReactDOM from 'react-dom'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { ManagementSetup } from 'src/plugins/management/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; import { CoreSetup, CoreStart, @@ -18,8 +27,10 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, } from '../constants'; import { JobId, JobStatusBuckets, NotificationsService } from '../index.d'; +import { ReportListing } from './components/report_listing'; import { getGeneralErrorToast } from './components'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; +import { JobQueueClient } from './lib/job_queue_client'; const { jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL }, @@ -49,11 +60,63 @@ function handleError( export class ReportingPublicPlugin implements Plugin { private readonly stop$ = new Rx.ReplaySubject(1); - // FIXME: License checking: only active, non-expired licenses allowed - // Depends on https://github.com/elastic/kibana/pull/44922 + private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { + defaultMessage: 'Reporting', + }); + + private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { + defaultMessage: 'Reporting', + }); + constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup) {} + public setup( + core: CoreSetup, + { + home, + management, + licensing, + }: { home: HomePublicPluginSetup; management: ManagementSetup; licensing: LicensingPluginSetup } + ) { + home.featureCatalogue.register({ + id: 'reporting', + title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { + defaultMessage: 'Reporting', + }), + description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { + defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', + }), + icon: 'reportingApp', + path: '/app/kibana#/management/kibana/reporting', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + + management.sections.getSection('kibana')!.registerApp({ + id: 'reporting', + title: this.title, + order: 15, + mount: async params => { + const [start] = await core.getStartServices(); + params.setBreadcrumbs([{ text: this.breadcrumbText }]); + ReactDOM.render( + + + , + params.element + ); + + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }); + } // FIXME: only perform these actions for authenticated routes // Depends on https://github.com/elastic/kibana/pull/39477 From 83b2b968b126f69a469020b945ee1612a6c872c9 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 27 Feb 2020 15:40:30 -0800 Subject: [PATCH 02/23] Fixing tests for report_listing snapshots --- x-pack/plugins/reporting/index.d.ts | 2 +- .../report_listing.test.tsx.snap | 456 ++++++++++++++++++ .../public/components/report_listing.test.tsx | 114 +++-- .../public/components/report_listing.tsx | 46 +- 4 files changed, 544 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index cd2def9bf10dc..6af391e4b4efb 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -13,7 +13,7 @@ import { NotificationsStart, } from '../../../src/core/public'; -export { ToastsSetup, HttpSetup } from 'src/core/public'; +export { ToastsSetup, HttpSetup, ApplicationStart } from 'src/core/public'; export type JobId = string; export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap new file mode 100644 index 0000000000000..b5304c6020c43 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -0,0 +1,456 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReportListing Report job listing with some items 1`] = ` +Array [ + +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Report + +
+
+
+ + Created at + +
+
+
+ + Status + +
+
+
+ + Actions + +
+
+
+ + Loading reports + +
+
+
+
+
+ , +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Report + +
+
+
+ + Created at + +
+
+
+ + Status + +
+
+
+ + Actions + +
+
+
+ + Loading reports + +
+
+
+
+
, +] +`; diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index d78eb5c409c1f..e36322f80e4f4 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -4,74 +4,82 @@ * you may not use this file except in compliance with the Elastic License. */ -interface JobData { - _index: string; - _id: string; - _source: { - browser_type: string; - created_at: string; - jobtype: string; - created_by: string; - payload: { - type: string; - title: string; - }; - kibana_name?: string; // undefined if job is pending (not yet claimed by an instance) - kibana_id?: string; // undefined if job is pending (not yet claimed by an instance) - output?: { content_type: string; size: number }; // undefined if job is incomplete - completed_at?: string; // undefined if job is incomplete - }; -} +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReportListing } from './report_listing'; +import { Observable } from 'rxjs'; +import { ILicense } from '../../../licensing/public'; +import { JobQueueClient } from '../lib/job_queue_client'; -jest.mock('ui/chrome', () => ({ - getInjected() { - return { - jobsRefresh: { - interval: 10, - intervalErrorMultiplier: 2, - }, - }; - }, -})); +const jobQueueClient = { + list: () => + Promise.resolve([ + { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore + ]), + total: () => Promise.resolve(18), +} as any; -jest.mock('ui/kfetch', () => ({ - kfetch: ({ pathname }: { pathname: string }): Promise => { - if (pathname === '/api/reporting/jobs/list') { - return Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]); - } +const validCheck = { + check: () => ({ + state: 'VALID', + message: '', + }), +}; - // query for jobs count - return Promise.resolve(18); +const license$ = { + subscribe: (handler: any) => { + return handler(validCheck); }, -})); +} as Observable; -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportListing } from './report_listing'; +const toasts = { + addDanger: jest.fn(), +} as any; describe('ReportListing', () => { it('Report job listing with some items', () => { const wrapper = mountWithIntl( ); wrapper.update(); const input = wrapper.find('[data-test-subj="reportJobListing"]'); expect(input).toMatchSnapshot(); }); + + it('subscribes to license changes, and unsubscribes on dismount', () => { + const unsubscribeMock = jest.fn(); + const subMock = { + subscribe: jest.fn().mockReturnValue({ + unsubscribe: unsubscribeMock, + }), + } as any; + + const wrapper = mountWithIntl( + } + redirect={jest.fn()} + toasts={toasts} + /> + ); + wrapper.update(); + expect(subMock.subscribe).toHaveBeenCalled(); + expect(unsubscribeMock).not.toHaveBeenCalled(); + wrapper.unmount(); + expect(unsubscribeMock).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 2618820f99de1..811103a5ec9f2 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -6,10 +6,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import moment from 'moment'; import { get } from 'lodash'; - +import moment from 'moment'; import React, { Component } from 'react'; +import { Subscription } from 'rxjs'; + import { EuiBasicTable, EuiButtonIcon, @@ -21,8 +22,8 @@ import { EuiToolTip, } from '@elastic/eui'; -import { ToastsSetup } from '../../'; -import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../../../licensing/public'; +import { ToastsSetup, ApplicationStart } from '../../'; +import { LicensingPluginSetup, LICENSE_CHECK_STATE, ILicense } from '../../../licensing/public'; import { Poller } from '../../common/poller'; import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; import { JobQueueClient, JobQueueEntry } from '../lib/job_queue_client'; @@ -47,12 +48,11 @@ interface Job { } interface Props { - redirect: (url: string) => void; intl: InjectedIntl; - // @TODO: Get right types here - toasts: ToastsSetup; jobQueueClient: JobQueueClient; license$: LicensingPluginSetup['license$']; + redirect: ApplicationStart['navigateToApp']; + toasts: ToastsSetup; } interface State { @@ -99,9 +99,10 @@ const jobStatusLabelsMap = new Map([ ]); class ReportListingUi extends Component { + private isInitialJobsFetch: boolean; + private licenseSubscription?: Subscription; private mounted?: boolean; private poller?: any; - private isInitialJobsFetch: boolean; constructor(props: Props) { super(props); @@ -117,18 +118,6 @@ class ReportListingUi extends Component { }; this.isInitialJobsFetch = true; - - this.props.license$.subscribe(license => { - const { state, message } = license.check('reporting', 'basic'); - const enableLinks = state === LICENSE_CHECK_STATE.Valid; - const showLinks = enableLinks && license.getFeature('csv').isAvailable; - - this.setState({ - enableLinks, - showLinks, - badLicenseMessage: message || '', - }); - }); } public render() { @@ -156,6 +145,10 @@ class ReportListingUi extends Component { public componentWillUnmount() { this.mounted = false; this.poller.stop(); + + if (this.licenseSubscription) { + this.licenseSubscription.unsubscribe(); + } } public componentDidMount() { @@ -172,8 +165,21 @@ class ReportListingUi extends Component { JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier, }); this.poller.start(); + this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); } + private licenseHandler = (license: ILicense) => { + const { state, message } = license.check('reporting', 'basic'); + const enableLinks = state === LICENSE_CHECK_STATE.Valid; + const showLinks = enableLinks; + + this.setState({ + enableLinks, + showLinks, + badLicenseMessage: message || '', + }); + }; + private renderTable() { const { intl } = this.props; From 5fd3a22f3f57b875c9c90c1bad9004f0e17e0a89 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 28 Feb 2020 09:38:51 -0800 Subject: [PATCH 03/23] WIP: Fixing react-component tests --- .../components/job_queue_client.test.mocks.ts | 17 +++++++++++ .../report_info_button.test.mocks.ts | 8 ------ .../components/report_info_button.test.tsx | 28 +++++++++++-------- .../public/components/report_info_button.tsx | 4 +-- 4 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts delete mode 100644 x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts diff --git a/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts b/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts new file mode 100644 index 0000000000000..de9a63eed38d1 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockJobQueueClient = { + http: jest.fn(), + list: jest.fn(), + total: jest.fn(), + getInfo: jest.fn(), + getContent: jest.fn(), + getReportURL: jest.fn(), + downloadReport: jest.fn(), +}; + +jest.mock('../lib/job_queue_client', () => mockJobQueueClient); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts b/x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts deleted file mode 100644 index 9dd7cbb5fc567..0000000000000 --- a/x-pack/plugins/reporting/public/components/report_info_button.test.mocks.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const mockJobQueueClient = { list: jest.fn(), total: jest.fn(), getInfo: jest.fn() }; -jest.mock('../lib/job_queue_client', () => ({ jobQueueClient: mockJobQueueClient })); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx index 3b9c2a8485423..726afcc37425f 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx @@ -4,27 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockJobQueueClient } from './report_info_button.test.mocks'; - import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportInfoButton } from './report_info_button'; +import { JobQueueClient } from '../lib/job_queue_client'; -describe('ReportInfoButton', () => { - beforeEach(() => { - mockJobQueueClient.getInfo = jest.fn(() => ({ - payload: { title: 'Test Job' }, - })); - }); +jest.mock('../lib/job_queue_client'); +const httpSetup = {} as any; +const mockJobQueueClient = new JobQueueClient(httpSetup); + +describe('ReportInfoButton', () => { it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); expect(input).toMatchSnapshot(); }); - it('opens flyout with info', () => { - const wrapper = mountWithIntl(); + it('opens flyout with info', async () => { + const wrapper = mountWithIntl( + + ); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -42,7 +44,9 @@ describe('ReportInfoButton', () => { throw new Error('Could not fetch the job info'); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx index 9531d4ce4ed8f..bd702296f54f5 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -259,13 +259,13 @@ export class ReportInfoButton extends Component { if (this.mounted) { this.setState({ isLoading: false, info }); } - } catch (kfetchError) { + } catch (err) { if (this.mounted) { this.setState({ isLoading: false, calloutTitle: 'Unable to fetch report info', info: null, - error: kfetchError, + error: err.message, }); } } From c02b023eea3b7a93e739ac39d344f92dc7a9565b Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 28 Feb 2020 13:07:13 -0800 Subject: [PATCH 04/23] Fixing report_info_button tests --- .../report_info_button.test.tsx.snap | 864 ++++++++++++++++++ .../public/components/report_info_button.tsx | 2 +- 2 files changed, 865 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap new file mode 100644 index 0000000000000..2055afdcf2bfe --- /dev/null +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap @@ -0,0 +1,864 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReportInfoButton handles button click flyout on click 1`] = ` + +`; + +exports[`ReportInfoButton opens flyout with fetch error info 1`] = ` +Array [ + + + + + } + /> + + + + +
+ +
+
+
+ + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + +
+ + + + +
+ +

+ Unable to fetch report info +

+
+
+
+ +
+
+ +
+ Could not fetch the job info +
+
+
+
+
+
+
+
+ +
+ + + + , +
+ + + + +
+ +

+ Unable to fetch report info +

+
+
+
+ +
+
+ +
+ Could not fetch the job info +
+
+
+
+
+
, +] +`; + +exports[`ReportInfoButton opens flyout with info 1`] = ` +Array [ + + + + + } + /> + + + + +
+ +
+
+
+ + + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + +
+ + + + +
+ +

+ Job Info +

+
+
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ + + + , +
+ + + + +
+ +

+ Job Info +

+
+
+
+ +
+
+ +
+ +
+
+ +
, +] +`; diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx index bd702296f54f5..3a5451d96f047 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -265,7 +265,7 @@ export class ReportInfoButton extends Component { isLoading: false, calloutTitle: 'Unable to fetch report info', info: null, - error: err.message, + error: err, }); } } From ede19d4e5b642a6107a99d4035d42bcbe07667da Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 28 Feb 2020 13:30:32 -0800 Subject: [PATCH 05/23] Fixing download linksies --- x-pack/plugins/reporting/public/lib/job_queue_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/plugins/reporting/public/lib/job_queue_client.ts index 2279623375e3a..3cca12968d958 100644 --- a/x-pack/plugins/reporting/public/lib/job_queue_client.ts +++ b/x-pack/plugins/reporting/public/lib/job_queue_client.ts @@ -59,7 +59,7 @@ export class JobQueueClient { public getReportURL(jobId: string) { const apiBaseUrl = this.http.basePath.prepend(API_BASE_URL); - const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`; + const downloadLink = `${apiBaseUrl}/download/${jobId}`; return downloadLink; } From e1f4a31661d94ae72129685dbd27ffd56a77fff5 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Wed, 4 Mar 2020 13:18:23 -0800 Subject: [PATCH 06/23] WIP: Final working implementation --- x-pack/legacy/plugins/reporting/index.ts | 5 - x-pack/plugins/reporting/constants.ts | 12 +- x-pack/plugins/reporting/index.d.ts | 2 +- x-pack/plugins/reporting/kibana.json | 2 +- .../components/job_queue_client.test.mocks.ts | 4 +- .../public/components/report_error_button.tsx | 8 +- .../components/report_info_button.test.tsx | 26 +- .../public/components/report_info_button.tsx | 6 +- .../public/components/report_listing.test.tsx | 8 +- .../public/components/report_listing.tsx | 14 +- .../components/reporting_panel_content.tsx | 266 ++++++++++++++++++ .../screen_capture_panel_content.tsx | 113 ++++++++ .../plugins/reporting/public/lib/job_queue.ts | 27 -- ...ueue_client.ts => reporting_api_client.ts} | 54 +++- .../public/lib/stream_handler.test.ts | 4 +- .../reporting/public/lib/stream_handler.ts | 47 ++-- .../panel_actions/get_csv_panel_action.tsx | 165 +++++++++++ x-pack/plugins/reporting/public/plugin.tsx | 70 ++++- .../register_csv_reporting.tsx | 97 +++++++ .../register_pdf_png_reporting.tsx | 179 ++++++++++++ 20 files changed, 992 insertions(+), 117 deletions(-) create mode 100644 x-pack/plugins/reporting/public/components/reporting_panel_content.tsx create mode 100644 x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx delete mode 100644 x-pack/plugins/reporting/public/lib/job_queue.ts rename x-pack/plugins/reporting/public/lib/{job_queue_client.ts => reporting_api_client.ts} (60%) create mode 100644 x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx create mode 100644 x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx create mode 100644 x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index b648bd276311d..c3ec6c4886f04 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -25,11 +25,6 @@ export const reporting = (kibana: any) => { config: reportingConfig, uiExports: { - shareContextMenuExtensions: [ - 'plugins/reporting/share_context_menu/register_csv_reporting', - 'plugins/reporting/share_context_menu/register_reporting', - ], - embeddableActions: ['plugins/reporting/panel_actions/get_csv_panel_action'], injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { const config = server.config(); return { diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 363cfd6b64b7a..41f5d9d8c1efd 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -14,10 +14,14 @@ export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { }, }; -export const API_BASE_URL = '/api/reporting/jobs'; +// Routes +export const API_BASE_URL = '/api/reporting'; +export const API_LIST_URL = `${API_BASE_URL}/jobs`; +export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/generate/immediate/csv/saved-object`; export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporting'; -export const API_LIST_URL = '/api/reporting/jobs'; +// Statuses export const JOB_STATUS_FAILED = 'failed'; export const JOB_STATUS_COMPLETED = 'completed'; @@ -29,8 +33,12 @@ export enum JobStatuses { CANCELLED = 'cancelled', } +// Types export const PDF_JOB_TYPE = 'printable_pdf'; export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; + +// Actions +export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 6af391e4b4efb..3b83a7b3d1251 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -13,7 +13,7 @@ import { NotificationsStart, } from '../../../src/core/public'; -export { ToastsSetup, HttpSetup, ApplicationStart } from 'src/core/public'; +export { ToastsSetup, HttpSetup, ApplicationStart, IUiSettingsClient } from 'src/core/public'; export type JobId = string; export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 2be3f51a09781..61ddbd58ba3c5 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,7 +2,7 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["home", "management", "licensing"], + "requiredPlugins": ["home", "management", "licensing", "uiActions", "embeddable", "share", "kibanaLegacy"], "server": false, "ui": true } diff --git a/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts b/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts index de9a63eed38d1..5e9614e27e2fd 100644 --- a/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts +++ b/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const mockJobQueueClient = { +export const mockAPIClient = { http: jest.fn(), list: jest.fn(), total: jest.fn(), @@ -14,4 +14,4 @@ export const mockJobQueueClient = { downloadReport: jest.fn(), }; -jest.mock('../lib/job_queue_client', () => mockJobQueueClient); +jest.mock('../lib/reporting_api_client', () => mockAPIClient); diff --git a/x-pack/plugins/reporting/public/components/report_error_button.tsx b/x-pack/plugins/reporting/public/components/report_error_button.tsx index b33ca2cb81436..252dee9c619a9 100644 --- a/x-pack/plugins/reporting/public/components/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_error_button.tsx @@ -7,12 +7,12 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobContent, JobQueueClient } from '../lib/job_queue_client'; +import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client'; interface Props { jobId: string; intl: InjectedIntl; - jobQueueClient: JobQueueClient; + apiClient: ReportingAPIClient; } interface State { @@ -91,9 +91,7 @@ class ReportErrorButtonUi extends Component { private loadError = async () => { this.setState({ isLoading: true }); try { - const reportContent: JobContent = await this.props.jobQueueClient.getContent( - this.props.jobId - ); + const reportContent: JobContent = await this.props.apiClient.getContent(this.props.jobId); if (this.mounted) { this.setState({ isLoading: false, error: reportContent.content }); } diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx index 726afcc37425f..07ffb1b77433e 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx @@ -7,26 +7,22 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportInfoButton } from './report_info_button'; -import { JobQueueClient } from '../lib/job_queue_client'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; jest.mock('../lib/job_queue_client'); const httpSetup = {} as any; -const mockJobQueueClient = new JobQueueClient(httpSetup); +const apiClient = new ReportingAPIClient(httpSetup); describe('ReportInfoButton', () => { it('handles button click flyout on click', () => { - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); expect(input).toMatchSnapshot(); }); it('opens flyout with info', async () => { - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -34,19 +30,17 @@ describe('ReportInfoButton', () => { const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); expect(flyout).toMatchSnapshot(); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-456'); + expect(apiClient.getInfo).toHaveBeenCalledTimes(1); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-456'); }); it('opens flyout with fetch error info', () => { // simulate fetch failure - mockJobQueueClient.getInfo = jest.fn(() => { + apiClient.getInfo = jest.fn(() => { throw new Error('Could not fetch the job info'); }); - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -54,7 +48,7 @@ describe('ReportInfoButton', () => { const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); expect(flyout).toMatchSnapshot(); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-789'); + expect(apiClient.getInfo).toHaveBeenCalledTimes(1); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-789'); }); }); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx index 3a5451d96f047..4026743fe7bc8 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -18,11 +18,11 @@ import { import React, { Component, Fragment } from 'react'; import { get } from 'lodash'; import { USES_HEADLESS_JOB_TYPES } from '../../constants'; -import { JobInfo, JobQueueClient } from '../lib/job_queue_client'; +import { JobInfo, ReportingAPIClient } from '../lib/reporting_api_client'; interface Props { jobId: string; - jobQueueClient: JobQueueClient; + apiClient: ReportingAPIClient; } interface State { @@ -255,7 +255,7 @@ export class ReportInfoButton extends Component { private loadInfo = async () => { this.setState({ isLoading: true }); try { - const info: JobInfo = await this.props.jobQueueClient.getInfo(this.props.jobId); + const info: JobInfo = await this.props.apiClient.getInfo(this.props.jobId); if (this.mounted) { this.setState({ isLoading: false, info }); } diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index e36322f80e4f4..5cf894580eae0 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -9,9 +9,9 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportListing } from './report_listing'; import { Observable } from 'rxjs'; import { ILicense } from '../../../licensing/public'; -import { JobQueueClient } from '../lib/job_queue_client'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; -const jobQueueClient = { +const reportingAPIClient = { list: () => Promise.resolve([ { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore @@ -49,7 +49,7 @@ describe('ReportListing', () => { it('Report job listing with some items', () => { const wrapper = mountWithIntl( { const wrapper = mountWithIntl( } redirect={jest.fn()} toasts={toasts} diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 811103a5ec9f2..a6710af7ccdb1 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -26,7 +26,7 @@ import { ToastsSetup, ApplicationStart } from '../../'; import { LicensingPluginSetup, LICENSE_CHECK_STATE, ILicense } from '../../../licensing/public'; import { Poller } from '../../common/poller'; import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; -import { JobQueueClient, JobQueueEntry } from '../lib/job_queue_client'; +import { ReportingAPIClient, JobQueueEntry } from '../lib/reporting_api_client'; import { ReportErrorButton } from './report_error_button'; import { ReportInfoButton } from './report_info_button'; @@ -49,7 +49,7 @@ interface Job { interface Props { intl: InjectedIntl; - jobQueueClient: JobQueueClient; + apiClient: ReportingAPIClient; license$: LicensingPluginSetup['license$']; redirect: ApplicationStart['navigateToApp']; toasts: ToastsSetup; @@ -350,7 +350,7 @@ class ReportListingUi extends Component { const { intl } = this.props; const button = ( this.props.jobQueueClient.downloadReport(record.id)} + onClick={() => this.props.apiClient.downloadReport(record.id)} iconType="importAction" aria-label={intl.formatMessage({ id: 'xpack.reporting.listing.table.downloadReportAriaLabel', @@ -396,11 +396,11 @@ class ReportListingUi extends Component { return; } - return ; + return ; }; private renderInfoButton = (record: Job) => { - return ; + return ; }; private onTableChange = ({ page }: { page: { index: number } }) => { @@ -417,8 +417,8 @@ class ReportListingUi extends Component { let jobs: JobQueueEntry[]; let total: number; try { - jobs = await this.props.jobQueueClient.list(this.state.page); - total = await this.props.jobQueueClient.total(); + jobs = await this.props.apiClient.list(this.state.page); + total = await this.props.apiClient.total(); this.isInitialJobsFetch = false; } catch (fetchError) { if (!this.licenseAllowsToShowThisPage()) { diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx new file mode 100644 index 0000000000000..9ca98664d081d --- /dev/null +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import React, { Component, ReactElement } from 'react'; +import url from 'url'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { ToastsSetup } from '../../'; + +interface Props { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + reportType: string; + layoutId: string | undefined; + objectId?: string; + objectType: string; + getJobParams: () => any; + options?: ReactElement; + isDirty: boolean; + onClose: () => void; + intl: InjectedIntl; +} + +interface State { + isStale: boolean; + absoluteUrl: string; + layoutId: string; +} + +class ReportingPanelContentUi extends Component { + private mounted?: boolean; + + constructor(props: Props) { + super(props); + + this.state = { + isStale: false, + absoluteUrl: '', + layoutId: '', + }; + } + + private getAbsoluteReportGenerationUrl = (props: Props) => { + const relativePath = this.props.apiClient.getReportingJobPath( + props.reportType, + props.getJobParams() + ); + return url.resolve(window.location.href, relativePath); + }; + + public getDerivedStateFromProps = (nextProps: Props, prevState: State) => { + if (nextProps.layoutId !== prevState.layoutId) { + return { + ...prevState, + absoluteUrl: this.getAbsoluteReportGenerationUrl(nextProps), + }; + } + return prevState; + }; + + public componentWillUnmount() { + window.removeEventListener('hashchange', this.markAsStale); + window.removeEventListener('resize', this.setAbsoluteReportGenerationUrl); + + this.mounted = false; + } + + public componentDidMount() { + this.mounted = true; + + window.addEventListener('hashchange', this.markAsStale, false); + window.addEventListener('resize', this.setAbsoluteReportGenerationUrl); + } + + public render() { + if (this.isNotSaved() || this.props.isDirty || this.state.isStale) { + return ( + + + } + > + {this.renderGenerateReportButton(true)} + + + ); + } + + const reportMsg = ( + + ); + + return ( + + +

{reportMsg}

+
+ + + {this.props.options} + + {this.renderGenerateReportButton(false)} + + + +

+ +

+
+ + + + {copy => ( + + + + )} + +
+ ); + } + + private renderGenerateReportButton = (isDisabled: boolean) => { + return ( + + + + ); + }; + + private prettyPrintReportingType = () => { + switch (this.props.reportType) { + case 'printablePdf': + return 'PDF'; + case 'csv': + return 'CSV'; + case 'png': + return 'PNG'; + default: + return this.props.reportType; + } + }; + + private markAsStale = () => { + if (!this.mounted) { + return; + } + + this.setState({ isStale: true }); + }; + + private isNotSaved = () => { + return this.props.objectId === undefined || this.props.objectId === ''; + }; + + private setAbsoluteReportGenerationUrl = () => { + if (!this.mounted) { + return; + } + const absoluteUrl = this.getAbsoluteReportGenerationUrl(this.props); + this.setState({ absoluteUrl }); + }; + + private createReportingJob = () => { + const { intl } = this.props; + + return this.props.apiClient + .createReportingJob(this.props.reportType, this.props.getJobParams()) + .then(() => { + this.props.toasts.addSuccess({ + title: intl.formatMessage( + { + id: 'xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle', + defaultMessage: 'Queued report for {objectType}', + }, + { objectType: this.props.objectType } + ), + text: toMountPoint( + + ), + 'data-test-subj': 'queueReportSuccess', + }); + this.props.onClose(); + }) + .catch((error: any) => { + if (error.message === 'not exportable') { + return this.props.toasts.addWarning({ + title: intl.formatMessage( + { + id: 'xpack.reporting.panelContent.whatCanBeExportedWarningTitle', + defaultMessage: 'Only saved {objectType} can be exported', + }, + { objectType: this.props.objectType } + ), + text: toMountPoint( + + ), + }); + } + + const defaultMessage = + error?.res?.status === 403 ? ( + + ) : ( + + ); + + this.props.toasts.addDanger({ + title: intl.formatMessage({ + id: 'xpack.reporting.panelContent.notification.reportingErrorTitle', + defaultMessage: 'Reporting error', + }), + text: toMountPoint(error.message || defaultMessage), + 'data-test-subj': 'queueReportError', + }); + }); + }; +} + +export const ReportingPanelContent = injectI18n(ReportingPanelContentUi); diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx new file mode 100644 index 0000000000000..b1ebb2583e5eb --- /dev/null +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component, Fragment } from 'react'; +import { ReportingPanelContent } from './reporting_panel_content'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ToastsSetup } from '../../'; + +interface Props { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + reportType: string; + objectId?: string; + objectType: string; + getJobParams: () => any; + isDirty: boolean; + onClose: () => void; +} + +interface State { + isPreserveLayoutSupported: boolean; + usePrintLayout: boolean; +} + +export class ScreenCapturePanelContent extends Component { + constructor(props: Props) { + super(props); + + const isPreserveLayoutSupported = + props.reportType !== 'png' && props.objectType !== 'visualization'; + this.state = { + isPreserveLayoutSupported, + usePrintLayout: false, + }; + } + + public render() { + return ( + + ); + } + + private renderOptions = () => { + if (this.state.isPreserveLayoutSupported) { + return ( + + + } + checked={this.state.usePrintLayout} + onChange={this.handlePrintLayoutChange} + data-test-subj="usePrintLayout" + /> + + + ); + } + + return ( + + + + ); + }; + + private handlePrintLayoutChange = (evt: any) => { + this.setState({ usePrintLayout: evt.target.checked }); + }; + + private getLayout = () => { + if (this.state.usePrintLayout) { + return { id: 'print' }; + } + + const el = document.querySelector('[data-shared-items-container]'); + const bounds = el ? el.getBoundingClientRect() : { height: 768, width: 1024 }; + + return { + id: this.props.reportType === 'png' ? 'png' : 'preserve_layout', + dimensions: { + height: bounds.height, + width: bounds.width, + }, + }; + }; + + private getJobParams = () => { + return { + ...this.props.getJobParams(), + layout: this.getLayout(), + }; + }; +} diff --git a/x-pack/plugins/reporting/public/lib/job_queue.ts b/x-pack/plugins/reporting/public/lib/job_queue.ts deleted file mode 100644 index 94a75051bf5b7..0000000000000 --- a/x-pack/plugins/reporting/public/lib/job_queue.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { HttpService, JobId, JobContent, SourceJob } from '../../index.d'; -import { API_BASE_URL } from '../../constants'; - -export class JobQueue { - public findForJobIds = (http: HttpService, jobIds: JobId[]): Promise => { - return http.fetch(`${API_BASE_URL}/list`, { - query: { page: 0, ids: jobIds.join(',') }, - method: 'GET', - }); - }; - - public getContent(http: HttpService, jobId: JobId): Promise { - return http - .fetch(`${API_BASE_URL}/output/${jobId}`, { - method: 'GET', - }) - .then((data: JobContent) => data.content); - } -} - -export const jobQueueClient = new JobQueue(); diff --git a/x-pack/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts similarity index 60% rename from x-pack/plugins/reporting/public/lib/job_queue_client.ts rename to x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 3cca12968d958..c843510637047 100644 --- a/x-pack/plugins/reporting/public/lib/job_queue_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -4,8 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { stringify } from 'query-string'; +import rison from 'rison-node'; + +import { add } from './job_completion_notifications'; import { HttpSetup } from '../../'; -import { API_LIST_URL, API_BASE_URL } from '../../constants'; +import { + API_LIST_URL, + API_BASE_URL, + API_BASE_GENERATE, + REPORTING_MANAGEMENT_HOME, +} from '../../constants'; +import { JobId, SourceJob } from '../..'; export interface JobQueueEntry { _id: string; @@ -50,7 +60,11 @@ export interface JobInfo { status: string; } -export class JobQueueClient { +interface JobParams { + [paramName: string]: any; +} + +export class ReportingAPIClient { private http: HttpSetup; constructor(http: HttpSetup) { @@ -58,7 +72,7 @@ export class JobQueueClient { } public getReportURL(jobId: string) { - const apiBaseUrl = this.http.basePath.prepend(API_BASE_URL); + const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); const downloadLink = `${apiBaseUrl}/download/${jobId}`; return downloadLink; @@ -100,4 +114,38 @@ export class JobQueueClient { asSystemRequest: true, }); } + + public findForJobIds = (jobIds: JobId[]): Promise => { + return this.http.fetch(`${API_LIST_URL}/list`, { + query: { page: 0, ids: jobIds.join(',') }, + method: 'GET', + }); + }; + + public getReportingJobPath = (exportType: string, jobParams: JobParams) => { + const params = stringify({ jobParams: rison.encode(jobParams) }); + + return `${this.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; + }; + + public createReportingJob = async (exportType: string, jobParams: any) => { + const jobParamsRison = rison.encode(jobParams); + const resp = await this.http.post(`${API_BASE_GENERATE}/${exportType}`, { + method: 'POST', + body: JSON.stringify({ + jobParams: jobParamsRison, + }), + }); + + add(resp.job.id); + + return resp; + }; + + public getManagementLink = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); + + public getDownloadLink = (jobId: JobId) => + this.http.basePath.prepend(`${API_LIST_URL}/download/${jobId}`); + + public getBasePath = () => this.http.basePath.get(); } diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index aeba2ca5406b8..5412e2545b146 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -7,7 +7,7 @@ import sinon, { stub } from 'sinon'; import { HttpSetup, NotificationsStart } from '../../../../../src/core/public'; import { SourceJob, JobSummary, HttpService } from '../../index.d'; -import { JobQueue } from './job_queue'; +import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; Object.defineProperty(window, 'sessionStorage', { @@ -44,7 +44,7 @@ const mockJobsFound = [ }, ]; -const jobQueueClientMock: JobQueue = { +const jobQueueClientMock: ReportingAPIClient = { findForJobIds: async (http: HttpService, jobIds: string[]) => { return mockJobsFound as SourceJob[]; }, diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index e58e90d3de8ef..1aae30f6fdfb0 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -11,19 +11,16 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, - API_BASE_URL, - REPORTING_MANAGEMENT_HOME, } from '../../constants'; + import { JobId, JobSummary, JobStatusBuckets, - HttpService, NotificationsService, SourceJob, - DownloadReportFn, - ManagementLinkFn, } from '../../index.d'; + import { getSuccessToast, getFailureToast, @@ -31,7 +28,7 @@ import { getWarningMaxSizeToast, getGeneralErrorToast, } from '../components'; -import { jobQueueClient as defaultJobQueueClient } from './job_queue'; +import { ReportingAPIClient } from './reporting_api_client'; function updateStored(jobIds: JobId[]): void { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds)); @@ -49,21 +46,7 @@ function summarizeJob(src: SourceJob): JobSummary { } export class ReportingNotifierStreamHandler { - private getManagementLink: ManagementLinkFn; - private getDownloadLink: DownloadReportFn; - - constructor( - private http: HttpService, - private notifications: NotificationsService, - private jobQueueClient = defaultJobQueueClient - ) { - this.getManagementLink = () => { - return http.basePath.prepend(REPORTING_MANAGEMENT_HOME); - }; - this.getDownloadLink = (jobId: JobId) => { - return http.basePath.prepend(`${API_BASE_URL}/download/${jobId}`); - }; - } + constructor(private notifications: NotificationsService, private apiClient: ReportingAPIClient) {} /* * Use Kibana Toast API to show our messages @@ -77,23 +60,33 @@ export class ReportingNotifierStreamHandler { for (const job of completedJobs) { if (job.csvContainsFormulas) { this.notifications.toasts.addWarning( - getWarningFormulasToast(job, this.getManagementLink, this.getDownloadLink) + getWarningFormulasToast( + job, + this.apiClient.getManagementLink, + this.apiClient.getDownloadLink + ) ); } else if (job.maxSizeReached) { this.notifications.toasts.addWarning( - getWarningMaxSizeToast(job, this.getManagementLink, this.getDownloadLink) + getWarningMaxSizeToast( + job, + this.apiClient.getManagementLink, + this.apiClient.getDownloadLink + ) ); } else { this.notifications.toasts.addSuccess( - getSuccessToast(job, this.getManagementLink, this.getDownloadLink) + getSuccessToast(job, this.apiClient.getManagementLink, this.apiClient.getDownloadLink) ); } } // no download link available for (const job of failedJobs) { - const content = await this.jobQueueClient.getContent(this.http, job.id); - this.notifications.toasts.addDanger(getFailureToast(content, job, this.getManagementLink)); + const { content } = await this.apiClient.getContent(job.id); + this.notifications.toasts.addDanger( + getFailureToast(content, job, this.apiClient.getManagementLink) + ); } return { completed: completedJobs, failed: failedJobs }; }; @@ -106,7 +99,7 @@ export class ReportingNotifierStreamHandler { * session storage) but have non-processing job status on the server */ public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable { - return Rx.from(this.jobQueueClient.findForJobIds(this.http, storedJobs)).pipe( + return Rx.from(this.apiClient.findForJobIds(storedJobs)).pipe( map((jobs: SourceJob[]) => { const completedJobs: JobSummary[] = []; const failedJobs: JobSummary[] = []; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx new file mode 100644 index 0000000000000..1bfccf1f135df --- /dev/null +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment-timezone'; + +import { CoreSetup } from '../../../../../src/core/public'; +import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; + +import { + ViewMode, + IEmbeddable, +} from '../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + +// @TODO: These will probably relocate at some point, and will need to be fixed +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; +import { ISearchEmbeddable } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types'; + +import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../constants'; + +function isSavedSearchEmbeddable( + embeddable: IEmbeddable | ISearchEmbeddable +): embeddable is ISearchEmbeddable { + return embeddable.type === SEARCH_EMBEDDABLE_TYPE; +} + +interface ActionContext { + embeddable: ISearchEmbeddable; +} + +export class GetCsvReportPanelAction implements Action { + private isDownloading: boolean; + public readonly type = CSV_REPORTING_ACTION; + public readonly id = CSV_REPORTING_ACTION; + private core: CoreSetup; + + constructor(core: CoreSetup) { + this.isDownloading = false; + this.core = core; + } + + public getIconType() { + return 'document'; + } + + public getDisplayName() { + return i18n.translate('xpack.reporting.dashboard.downloadCsvPanelTitle', { + defaultMessage: 'Download CSV', + }); + } + + public async getSearchRequestBody({ searchEmbeddable }: { searchEmbeddable: any }) { + const adapters = searchEmbeddable.getInspectorAdapters(); + if (!adapters) { + return {}; + } + + if (adapters.requests.requests.length === 0) { + return {}; + } + + return searchEmbeddable.getSavedSearch().searchSource.getSearchRequestBody(); + } + + public isCompatible = async (context: ActionContext) => { + const { embeddable } = context; + + return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; + }; + + public execute = async (context: ActionContext) => { + const { embeddable } = context; + + if (!isSavedSearchEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + + if (this.isDownloading) { + return; + } + + const { + timeRange: { to, from }, + } = embeddable.getInput(); + + const searchEmbeddable = embeddable; + const searchRequestBody = await this.getSearchRequestBody({ searchEmbeddable }); + const state = _.pick(searchRequestBody, ['sort', 'docvalue_fields', 'query']); + const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); + + const id = `search:${embeddable.getSavedSearch().id}`; + const filename = embeddable.getTitle(); + const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; + const fromTime = dateMath.parse(from); + const toTime = dateMath.parse(to); + + if (!fromTime || !toTime) { + return this.onGenerationFail( + new Error(`Invalid time range: From: ${fromTime}, To: ${toTime}`) + ); + } + + const body = JSON.stringify({ + timerange: { + min: fromTime.format(), + max: toTime.format(), + timezone, + }, + state, + }); + + this.isDownloading = true; + + this.core.notifications.toasts.addSuccess({ + title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', { + defaultMessage: `CSV Download Started`, + }), + text: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedMessage', { + defaultMessage: `Your CSV will download momentarily.`, + }), + 'data-test-subj': 'csvDownloadStarted', + }); + + await this.core.http + .post(`${API_GENERATE_IMMEDIATE}/${id}`, { body }) + .then((rawResponse: string) => { + this.isDownloading = false; + + const download = `${filename}.csv`; + const blob = new Blob([rawResponse], { type: 'text/csv;charset=utf-8;' }); + + // Hack for IE11 Support + if (window.navigator.msSaveOrOpenBlob) { + return window.navigator.msSaveOrOpenBlob(blob, download); + } + + const a = window.document.createElement('a'); + const downloadObject = window.URL.createObjectURL(blob); + + a.href = downloadObject; + a.download = download; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(downloadObject); + document.body.removeChild(a); + }) + .catch(this.onGenerationFail.bind(this)); + }; + + private onGenerationFail(error: Error) { + this.isDownloading = false; + this.core.notifications.toasts.addDanger({ + title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', { + defaultMessage: `CSV download failed`, + }), + text: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadMessage', { + defaultMessage: `We couldn't generate your CSV at this time.`, + }), + 'data-test-subj': 'downloadCsvFail', + }); + } +} diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index fea8750ea7f23..8a6d1b463229b 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -11,26 +11,39 @@ import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { ManagementSetup } from 'src/plugins/management/public'; import { I18nProvider } from '@kbn/i18n/react'; +import { UiActionsSetup } from 'src/plugins/ui_actions/public'; + +import { ReportListing } from './components/report_listing'; +import { getGeneralErrorToast } from './components'; + +import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; +import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; +import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; + +import { LicensingPluginSetup } from '../../licensing/public'; +import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; +import { SharePluginSetup } from '../../../../src/plugins/share/public'; + import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { LicensingPluginSetup } from '../../licensing/public'; + import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, } from '../../../../src/core/public'; + import { JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG, JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, } from '../constants'; -import { JobId, JobStatusBuckets, NotificationsService } from '../index.d'; -import { ReportListing } from './components/report_listing'; -import { getGeneralErrorToast } from './components'; -import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; -import { JobQueueClient } from './lib/job_queue_client'; + +import { JobId, JobStatusBuckets, NotificationsService } from '..'; const { jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL }, @@ -76,8 +89,27 @@ export class ReportingPublicPlugin implements Plugin { home, management, licensing, - }: { home: HomePublicPluginSetup; management: ManagementSetup; licensing: LicensingPluginSetup } + uiActions, + share, + }: { + home: HomePublicPluginSetup; + management: ManagementSetup; + licensing: LicensingPluginSetup; + uiActions: UiActionsSetup; + share: SharePluginSetup; + } ) { + const { + http, + notifications: { toasts }, + getStartServices, + uiSettings, + } = core; + const { license$ } = licensing; + + const apiClient = new ReportingAPIClient(http); + const action = new GetCsvReportPanelAction(core); + home.featureCatalogue.register({ id: 'reporting', title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { @@ -97,15 +129,15 @@ export class ReportingPublicPlugin implements Plugin { title: this.title, order: 15, mount: async params => { - const [start] = await core.getStartServices(); + const [start] = await getStartServices(); params.setBreadcrumbs([{ text: this.breadcrumbText }]); ReactDOM.render( , params.element @@ -116,13 +148,27 @@ export class ReportingPublicPlugin implements Plugin { }; }, }); + + uiActions.registerAction(action); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, action.id); + + share.register(csvReportingProvider({ apiClient, toasts, license$ })); + share.register( + reportingPDFPNGProvider({ + apiClient, + toasts, + license$, + uiSettings, + }) + ); } // FIXME: only perform these actions for authenticated routes // Depends on https://github.com/elastic/kibana/pull/39477 public start(core: CoreStart) { const { http, notifications } = core; - const streamHandler = new StreamHandler(http, notifications); + const apiClient = new ReportingAPIClient(http); + const streamHandler = new StreamHandler(notifications, apiClient); Rx.timer(0, JOBS_REFRESH_INTERVAL) .pipe( diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx new file mode 100644 index 0000000000000..eee3c4da1cb47 --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +import { ReportingPanelContent } from '../components/reporting_panel_content'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../../../licensing/public'; +import { ShareContext } from '../../../../../src/plugins/share/public'; +import { ToastsSetup } from '../..'; + +interface ReportingProvider { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + license$: LicensingPluginSetup['license$']; +} + +export const csvReportingProvider = ({ apiClient, toasts, license$ }: ReportingProvider) => { + let toolTipContent = ''; + let disabled = true; + let hasCSVReporting = false; + + license$.subscribe(license => { + const { state, message = '' } = license.check('reporting', 'basic'); + const enableLinks = state === LICENSE_CHECK_STATE.Valid; + + toolTipContent = message; + hasCSVReporting = enableLinks; + disabled = !enableLinks; + }); + + const getShareMenuItems = ({ + objectType, + objectId, + sharingData, + isDirty, + onClose, + }: ShareContext) => { + if ('search' !== objectType) { + return []; + } + + const getJobParams = () => { + return { + ...sharingData, + type: objectType, + }; + }; + + const shareActions = []; + + if (hasCSVReporting) { + const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.csvReportsButtonLabel', { + defaultMessage: 'CSV Reports', + }); + + shareActions.push({ + shareMenuItem: { + name: panelTitle, + icon: 'document', + toolTipContent, + disabled, + ['data-test-subj']: 'csvReportMenuItem', + sortOrder: 1, + }, + panel: { + id: 'csvReportingPanel', + title: panelTitle, + content: ( + + ), + }, + }); + } + + return shareActions; + }; + + return { + id: 'csvReports', + getShareMenuItems, + }; +}; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx new file mode 100644 index 0000000000000..04c804c96399a --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import moment from 'moment-timezone'; +import React from 'react'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; +import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../../../licensing/public'; +import { ShareContext } from '../../../../../src/plugins/share/public'; +import { ToastsSetup, IUiSettingsClient } from '../../'; + +interface ReportingPDFPNGProvider { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + license$: LicensingPluginSetup['license$']; + uiSettings: IUiSettingsClient; +} + +export const reportingPDFPNGProvider = ({ + apiClient, + toasts, + license$, + uiSettings, +}: ReportingPDFPNGProvider) => { + let toolTipContent = ''; + let disabled = true; + let hasPDFPNGReporting = false; + + license$.subscribe(license => { + const { state, message = '' } = license.check('reporting', 'gold'); + const enableLinks = state === LICENSE_CHECK_STATE.Valid; + + toolTipContent = message; + hasPDFPNGReporting = enableLinks; + disabled = !enableLinks; + }); + + const getShareMenuItems = ({ + objectType, + objectId, + sharingData, + isDirty, + onClose, + shareableUrl, + }: ShareContext) => { + if (!['dashboard', 'visualization'].includes(objectType)) { + return []; + } + // Dashboard only mode does not currently support reporting + // https://github.com/elastic/kibana/issues/18286 + // @TODO For NP + if (objectType === 'dashboard' && false) { + return []; + } + + const getReportingJobParams = () => { + // Replace hashes with original RISON values. + const relativeUrl = shareableUrl.replace( + window.location.origin + apiClient.getBasePath(), + '' + ); + + const browserTimezone = + uiSettings.get('dateFormat:tz') === 'Browser' + ? moment.tz.guess() + : uiSettings.get('dateFormat:tz'); + + return { + ...sharingData, + objectType, + browserTimezone, + relativeUrls: [relativeUrl], + }; + }; + + const getPngJobParams = () => { + // Replace hashes with original RISON values. + const relativeUrl = shareableUrl.replace( + window.location.origin + apiClient.getBasePath(), + '' + ); + + const browserTimezone = + uiSettings.get('dateFormat:tz') === 'Browser' + ? moment.tz.guess() + : uiSettings.get('dateFormat:tz'); + + return { + ...sharingData, + objectType, + browserTimezone, + relativeUrl, + }; + }; + + const shareActions = []; + + if (hasPDFPNGReporting) { + const pngPanelTitle = i18n.translate( + 'xpack.reporting.shareContextMenu.pngReportsButtonLabel', + { + defaultMessage: 'PNG Reports', + } + ); + + const pdfPanelTitle = i18n.translate( + 'xpack.reporting.shareContextMenu.pdfReportsButtonLabel', + { + defaultMessage: 'PDF Reports', + } + ); + + shareActions.push({ + shareMenuItem: { + name: pngPanelTitle, + icon: 'document', + toolTipContent, + disabled, + ['data-test-subj']: 'pngReportMenuItem', + sortOrder: 10, + }, + panel: { + id: 'reportingPngPanel', + title: pngPanelTitle, + content: ( + + ), + }, + }); + + shareActions.push({ + shareMenuItem: { + name: pdfPanelTitle, + icon: 'document', + toolTipContent, + disabled, + ['data-test-subj']: 'pdfReportMenuItem', + sortOrder: 10, + }, + panel: { + id: 'reportingPdfPanel', + title: pdfPanelTitle, + content: ( + + ), + }, + }); + } + + return shareActions; + }; + + return { + id: 'screenCaptureReports', + getShareMenuItems, + }; +}; From 63222289162a4070e51d47fba048acbd9255553d Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 6 Mar 2020 09:22:29 -0800 Subject: [PATCH 07/23] Fixing attachAction API + API URLs --- x-pack/plugins/reporting/constants.ts | 2 +- x-pack/plugins/reporting/public/plugin.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 41f5d9d8c1efd..843ba23e27e44 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -18,7 +18,7 @@ export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { export const API_BASE_URL = '/api/reporting'; export const API_LIST_URL = `${API_BASE_URL}/jobs`; export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/generate/immediate/csv/saved-object`; +export const API_GENERATE_IMMEDIATE = `${API_BASE_GENERATE}/v1/generate/immediate/csv/saved-object`; export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporting'; // Statuses diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 8a6d1b463229b..bced9d6ddd32a 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -150,7 +150,7 @@ export class ReportingPublicPlugin implements Plugin { }); uiActions.registerAction(action); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, action.id); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); share.register(csvReportingProvider({ apiClient, toasts, license$ })); share.register( From ca80be248725ae597c8ed01c84bad8c2ddfbec94 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 6 Mar 2020 10:15:29 -0800 Subject: [PATCH 08/23] =?UTF-8?q?Let=20the=20past=20die.=20Kill=20it=20if?= =?UTF-8?q?=20you=20have=20to.=20That=E2=80=99s=20the=20only=20way=20to=20?= =?UTF-8?q?become=20what=20you=20were=20meant=20to=20be.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workpad_header/workpad_export/index.ts | 2 +- .../report_info_button.test.tsx.snap | 896 ------------------ .../report_listing.test.tsx.snap | 456 --------- .../public/components/report_error_button.tsx | 112 --- .../report_info_button.test.mocks.ts | 8 - .../components/report_info_button.test.tsx | 56 -- .../public/components/report_info_button.tsx | 294 ------ .../public/components/report_listing.test.tsx | 77 -- .../public/components/report_listing.tsx | 479 ---------- .../components/reporting_panel_content.tsx | 263 ----- .../screen_capture_panel_content.tsx | 107 --- .../public/constants/job_statuses.tsx | 13 - .../reporting/public/lib/download_report.ts | 23 - .../lib/job_completion_notifications.ts | 36 - .../reporting/public/lib/job_queue_client.ts | 89 -- .../reporting/public/lib/reporting_client.ts | 37 - .../panel_actions/get_csv_panel_action.tsx | 178 ---- .../reporting/public/register_feature.ts | 27 - .../register_csv_reporting.tsx | 76 -- .../share_context_menu/register_reporting.tsx | 153 --- .../public/views/management/index.js | 7 - .../public/views/management/jobs.html | 3 - .../reporting/public/views/management/jobs.js | 59 -- .../public/views/management/management.js | 44 - x-pack/plugins/reporting/public/index.ts | 2 + 25 files changed, 3 insertions(+), 3494 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_listing.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/download_report.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/register_feature.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/index.js delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/jobs.html delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/jobs.js delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/management.js diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 7f81adad6bf9b..949264fcc9fdb 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import * as jobCompletionNotifications from '../../../../../reporting/public/lib/job_completion_notifications'; +import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; // @ts-ignore Untyped local import { getWorkpad, getPages } from '../../../state/selectors/workpad'; // @ts-ignore Untyped local diff --git a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap b/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap deleted file mode 100644 index f89e90cc4860c..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap +++ /dev/null @@ -1,896 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReportInfoButton handles button click flyout on click 1`] = ` - -`; - -exports[`ReportInfoButton opens flyout with fetch error info 1`] = ` -Array [ - - - - - } - /> - - - - -
- -
-
-
- - -
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - > - - -
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
- - - - -
- -

- Unable to fetch report info -

-
-
-
- -
-
-
- -
- Could not fetch the job info -
-
-
-
-
-
-
-
-
- -
- - - - , -
- - - - -
- -

- Unable to fetch report info -

-
-
-
- -
-
-
- -
- Could not fetch the job info -
-
-
-
-
-
-
, -] -`; - -exports[`ReportInfoButton opens flyout with info 1`] = ` -Array [ - - - - - } - /> - - - - -
- -
-
-
- - - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - > - - - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
- - - - -
- -

- Job Info -

-
-
-
- -
-
-
- -
- -
-
-
- -
-
-
- -
- - - - , -
- - - - -
- -

- Job Info -

-
-
-
- -
-
-
- -
- -
-
-
- -
, -] -`; diff --git a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap deleted file mode 100644 index b5304c6020c43..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ /dev/null @@ -1,456 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReportListing Report job listing with some items 1`] = ` -Array [ - -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - Report - -
-
-
- - Created at - -
-
-
- - Status - -
-
-
- - Actions - -
-
-
- - Loading reports - -
-
-
-
-
- , -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - Report - -
-
-
- - Created at - -
-
-
- - Status - -
-
-
- - Actions - -
-
-
- - Loading reports - -
-
-
-
-
, -] -`; diff --git a/x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx b/x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx deleted file mode 100644 index 3e6fd07847f2c..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import { JobContent, jobQueueClient } from '../lib/job_queue_client'; - -interface Props { - jobId: string; - intl: InjectedIntl; -} - -interface State { - isLoading: boolean; - isPopoverOpen: boolean; - calloutTitle: string; - error?: string; -} - -class ReportErrorButtonUi extends Component { - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isPopoverOpen: false, - calloutTitle: props.intl.formatMessage({ - id: 'xpack.reporting.errorButton.unableToGenerateReportTitle', - defaultMessage: 'Unable to generate report', - }), - }; - } - - public render() { - const button = ( - - ); - - return ( - - -

{this.state.error}

-
-
- ); - } - - public componentWillUnmount() { - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - } - - private togglePopover = () => { - this.setState(prevState => { - return { isPopoverOpen: !prevState.isPopoverOpen }; - }); - - if (!this.state.error) { - this.loadError(); - } - }; - - private closePopover = () => { - this.setState({ isPopoverOpen: false }); - }; - - private loadError = async () => { - this.setState({ isLoading: true }); - try { - const reportContent: JobContent = await jobQueueClient.getContent(this.props.jobId); - if (this.mounted) { - this.setState({ isLoading: false, error: reportContent.content }); - } - } catch (kfetchError) { - if (this.mounted) { - this.setState({ - isLoading: false, - calloutTitle: this.props.intl.formatMessage({ - id: 'xpack.reporting.errorButton.unableToFetchReportContentTitle', - defaultMessage: 'Unable to fetch report content', - }), - error: kfetchError.message, - }); - } - } - }; -} - -export const ReportErrorButton = injectI18n(ReportErrorButtonUi); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts b/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts deleted file mode 100644 index 9dd7cbb5fc567..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const mockJobQueueClient = { list: jest.fn(), total: jest.fn(), getInfo: jest.fn() }; -jest.mock('../lib/job_queue_client', () => ({ jobQueueClient: mockJobQueueClient })); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx deleted file mode 100644 index 3b9c2a8485423..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mockJobQueueClient } from './report_info_button.test.mocks'; - -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportInfoButton } from './report_info_button'; - -describe('ReportInfoButton', () => { - beforeEach(() => { - mockJobQueueClient.getInfo = jest.fn(() => ({ - payload: { title: 'Test Job' }, - })); - }); - - it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - expect(input).toMatchSnapshot(); - }); - - it('opens flyout with info', () => { - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - - input.simulate('click'); - - const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); - expect(flyout).toMatchSnapshot(); - - expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-456'); - }); - - it('opens flyout with fetch error info', () => { - // simulate fetch failure - mockJobQueueClient.getInfo = jest.fn(() => { - throw new Error('Could not fetch the job info'); - }); - - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - - input.simulate('click'); - - const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); - expect(flyout).toMatchSnapshot(); - - expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-789'); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx b/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx deleted file mode 100644 index 7f5d070948e50..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButtonIcon, - EuiDescriptionList, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPortal, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import React, { Component, Fragment } from 'react'; -import { get } from 'lodash'; -import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; -import { JobInfo, jobQueueClient } from '../lib/job_queue_client'; - -interface Props { - jobId: string; -} - -interface State { - isLoading: boolean; - isFlyoutVisible: boolean; - calloutTitle: string; - info: JobInfo | null; - error: Error | null; -} - -const NA = 'n/a'; -const UNKNOWN = 'unknown'; - -const getDimensions = (info: JobInfo): string => { - const defaultDimensions = { width: null, height: null }; - const { width, height } = get(info, 'payload.layout.dimensions', defaultDimensions); - if (width && height) { - return `Width: ${width} x Height: ${height}`; - } - return NA; -}; - -export class ReportInfoButton extends Component { - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isFlyoutVisible: false, - calloutTitle: 'Job Info', - info: null, - error: null, - }; - - this.closeFlyout = this.closeFlyout.bind(this); - this.showFlyout = this.showFlyout.bind(this); - } - - public renderInfo() { - const { info, error: err } = this.state; - if (err) { - return err.message; - } - if (!info) { - return null; - } - - const jobType = info.jobtype || NA; - - interface JobInfo { - title: string; - description: string; - } - - interface JobInfoMap { - [thing: string]: JobInfo[]; - } - - const attempts = info.attempts ? info.attempts.toString() : NA; - const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; - const priority = info.priority ? info.priority.toString() : NA; - const timeout = info.timeout ? info.timeout.toString() : NA; - const warnings = info.output && info.output.warnings ? info.output.warnings.join(',') : null; - - const jobInfoDateTimes: JobInfo[] = [ - { - title: 'Created By', - description: info.created_by || NA, - }, - { - title: 'Created At', - description: info.created_at || NA, - }, - { - title: 'Started At', - description: info.started_at || NA, - }, - { - title: 'Completed At', - description: info.completed_at || NA, - }, - { - title: 'Processed By', - description: - info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : UNKNOWN, - }, - { - title: 'Browser Timezone', - description: get(info, 'payload.browserTimezone') || NA, - }, - ]; - const jobInfoPayload: JobInfo[] = [ - { - title: 'Title', - description: get(info, 'payload.title') || NA, - }, - { - title: 'Type', - description: get(info, 'payload.type') || NA, - }, - { - title: 'Layout', - description: get(info, 'meta.layout') || NA, - }, - { - title: 'Dimensions', - description: getDimensions(info), - }, - { - title: 'Job Type', - description: jobType, - }, - { - title: 'Content Type', - description: get(info, 'output.content_type') || NA, - }, - { - title: 'Size in Bytes', - description: get(info, 'output.size') || NA, - }, - ]; - const jobInfoStatus: JobInfo[] = [ - { - title: 'Attempts', - description: attempts, - }, - { - title: 'Max Attempts', - description: maxAttempts, - }, - { - title: 'Priority', - description: priority, - }, - { - title: 'Timeout', - description: timeout, - }, - { - title: 'Status', - description: info.status || NA, - }, - { - title: 'Browser Type', - description: USES_HEADLESS_JOB_TYPES.includes(jobType) ? info.browser_type || UNKNOWN : NA, - }, - ]; - if (warnings) { - jobInfoStatus.push({ - title: 'Errors', - description: warnings, - }); - } - - const jobInfoParts: JobInfoMap = { - datetimes: jobInfoDateTimes, - payload: jobInfoPayload, - status: jobInfoStatus, - }; - - return ( - - - - - - - - ); - } - - public componentWillUnmount() { - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - } - - public render() { - let flyout; - - if (this.state.isFlyoutVisible) { - flyout = ( - - - - -

{this.state.calloutTitle}

-
-
- - {this.renderInfo()} - -
-
- ); - } - - return ( - - - {flyout} - - ); - } - - private loadInfo = async () => { - this.setState({ isLoading: true }); - try { - const info: JobInfo = await jobQueueClient.getInfo(this.props.jobId); - if (this.mounted) { - this.setState({ isLoading: false, info }); - } - } catch (kfetchError) { - if (this.mounted) { - this.setState({ - isLoading: false, - calloutTitle: 'Unable to fetch report info', - info: null, - error: kfetchError, - }); - } - } - }; - - private closeFlyout = () => { - this.setState({ - isFlyoutVisible: false, - info: null, // force re-read for next click - }); - }; - - private showFlyout = () => { - this.setState({ isFlyoutVisible: true }); - - if (!this.state.info) { - this.loadInfo(); - } - }; -} diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx deleted file mode 100644 index d78eb5c409c1f..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -interface JobData { - _index: string; - _id: string; - _source: { - browser_type: string; - created_at: string; - jobtype: string; - created_by: string; - payload: { - type: string; - title: string; - }; - kibana_name?: string; // undefined if job is pending (not yet claimed by an instance) - kibana_id?: string; // undefined if job is pending (not yet claimed by an instance) - output?: { content_type: string; size: number }; // undefined if job is incomplete - completed_at?: string; // undefined if job is incomplete - }; -} - -jest.mock('ui/chrome', () => ({ - getInjected() { - return { - jobsRefresh: { - interval: 10, - intervalErrorMultiplier: 2, - }, - }; - }, -})); - -jest.mock('ui/kfetch', () => ({ - kfetch: ({ pathname }: { pathname: string }): Promise => { - if (pathname === '/api/reporting/jobs/list') { - return Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]); - } - - // query for jobs count - return Promise.resolve(18); - }, -})); - -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportListing } from './report_listing'; - -describe('ReportListing', () => { - it('Report job listing with some items', () => { - const wrapper = mountWithIntl( - - ); - wrapper.update(); - const input = wrapper.find('[data-test-subj="reportJobListing"]'); - expect(input).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx deleted file mode 100644 index 54061eda94dce..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import moment from 'moment'; -import { get } from 'lodash'; -import React, { Component } from 'react'; -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; -import { - EuiBasicTable, - EuiButtonIcon, - EuiPageContent, - EuiSpacer, - EuiText, - EuiTextColor, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { Poller } from '../../../../common/poller'; -import { JobStatuses } from '../constants/job_statuses'; -import { downloadReport } from '../lib/download_report'; -import { jobQueueClient, JobQueueEntry } from '../lib/job_queue_client'; -import { ReportErrorButton } from './report_error_button'; -import { ReportInfoButton } from './report_info_button'; - -interface Job { - id: string; - type: string; - object_type: string; - object_title: string; - created_by?: string; - created_at: string; - started_at?: string; - completed_at?: string; - status: string; - statusLabel: string; - max_size_reached: boolean; - attempts: number; - max_attempts: number; - csv_contains_formulas: boolean; - warnings: string[]; -} - -interface Props { - badLicenseMessage: string; - showLinks: boolean; - enableLinks: boolean; - redirect: (url: string) => void; - intl: InjectedIntl; -} - -interface State { - page: number; - total: number; - jobs: Job[]; - isLoading: boolean; -} - -const jobStatusLabelsMap = new Map([ - [ - JobStatuses.PENDING, - i18n.translate('xpack.reporting.jobStatuses.pendingText', { - defaultMessage: 'Pending', - }), - ], - [ - JobStatuses.PROCESSING, - i18n.translate('xpack.reporting.jobStatuses.processingText', { - defaultMessage: 'Processing', - }), - ], - [ - JobStatuses.COMPLETED, - i18n.translate('xpack.reporting.jobStatuses.completedText', { - defaultMessage: 'Completed', - }), - ], - [ - JobStatuses.FAILED, - i18n.translate('xpack.reporting.jobStatuses.failedText', { - defaultMessage: 'Failed', - }), - ], - [ - JobStatuses.CANCELLED, - i18n.translate('xpack.reporting.jobStatuses.cancelledText', { - defaultMessage: 'Cancelled', - }), - ], -]); - -class ReportListingUi extends Component { - private mounted?: boolean; - private poller?: any; - private isInitialJobsFetch: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - page: 0, - total: 0, - jobs: [], - isLoading: false, - }; - - this.isInitialJobsFetch = true; - } - - public render() { - return ( - - -

- -

-
- -

- -

-
- - {this.renderTable()} -
- ); - } - - public componentWillUnmount() { - this.mounted = false; - this.poller.stop(); - } - - public componentDidMount() { - this.mounted = true; - const { jobsRefresh } = chrome.getInjected('reportingPollConfig'); - this.poller = new Poller({ - functionToPoll: () => { - return this.fetchJobs(); - }, - pollFrequencyInMillis: jobsRefresh.interval, - trailing: false, - continuePollingOnError: true, - pollFrequencyErrorMultiplier: jobsRefresh.intervalErrorMultiplier, - }); - this.poller.start(); - } - - private renderTable() { - const { intl } = this.props; - - const tableColumns = [ - { - field: 'object_title', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.reportTitle', - defaultMessage: 'Report', - }), - render: (objectTitle: string, record: Job) => { - return ( -
-
{objectTitle}
- - {record.object_type} - -
- ); - }, - }, - { - field: 'created_at', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.createdAtTitle', - defaultMessage: 'Created at', - }), - render: (createdAt: string, record: Job) => { - if (record.created_by) { - return ( -
-
{this.formatDate(createdAt)}
- {record.created_by} -
- ); - } - return this.formatDate(createdAt); - }, - }, - { - field: 'status', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.statusTitle', - defaultMessage: 'Status', - }), - render: (status: string, record: Job) => { - if (status === 'pending') { - return ( -
- -
- ); - } - - let maxSizeReached; - if (record.max_size_reached) { - maxSizeReached = ( - - - - ); - } - - let warnings; - if (record.warnings) { - warnings = ( - - - - - - ); - } - - let statusTimestamp; - if (status === JobStatuses.PROCESSING && record.started_at) { - statusTimestamp = this.formatDate(record.started_at); - } else if ( - record.completed_at && - (status === JobStatuses.COMPLETED || status === JobStatuses.FAILED) - ) { - statusTimestamp = this.formatDate(record.completed_at); - } - - let statusLabel = jobStatusLabelsMap.get(status as JobStatuses) || status; - - if (status === JobStatuses.PROCESSING) { - statusLabel = statusLabel + ` (attempt ${record.attempts} of ${record.max_attempts})`; - } - - if (statusTimestamp) { - return ( -
- {statusTimestamp}, - }} - /> - {maxSizeReached} - {warnings} -
- ); - } - - // unknown status - return ( -
- {statusLabel} - {maxSizeReached} - {warnings} -
- ); - }, - }, - { - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.actionsTitle', - defaultMessage: 'Actions', - }), - actions: [ - { - render: (record: Job) => { - return ( -
- {this.renderDownloadButton(record)} - {this.renderReportErrorButton(record)} - {this.renderInfoButton(record)} -
- ); - }, - }, - ], - }, - ]; - - const pagination = { - pageIndex: this.state.page, - pageSize: 10, - totalItemCount: this.state.total, - hidePerPageOptions: true, - }; - - return ( - - ); - } - - private renderDownloadButton = (record: Job) => { - if (record.status !== JobStatuses.COMPLETED) { - return; - } - - const { intl } = this.props; - const button = ( - downloadReport(record.id)} - iconType="importAction" - aria-label={intl.formatMessage({ - id: 'xpack.reporting.listing.table.downloadReportAriaLabel', - defaultMessage: 'Download report', - })} - /> - ); - - if (record.csv_contains_formulas) { - return ( - - {button} - - ); - } - - if (record.max_size_reached) { - return ( - - {button} - - ); - } - - return button; - }; - - private renderReportErrorButton = (record: Job) => { - if (record.status !== JobStatuses.FAILED) { - return; - } - - return ; - }; - - private renderInfoButton = (record: Job) => { - return ; - }; - - private onTableChange = ({ page }: { page: { index: number } }) => { - const { index: pageIndex } = page; - this.setState(() => ({ page: pageIndex }), this.fetchJobs); - }; - - private fetchJobs = async () => { - // avoid page flicker when poller is updating table - only display loading screen on first load - if (this.isInitialJobsFetch) { - this.setState(() => ({ isLoading: true })); - } - - let jobs: JobQueueEntry[]; - let total: number; - try { - jobs = await jobQueueClient.list(this.state.page); - total = await jobQueueClient.total(); - this.isInitialJobsFetch = false; - } catch (kfetchError) { - if (!this.licenseAllowsToShowThisPage()) { - toastNotifications.addDanger(this.props.badLicenseMessage); - this.props.redirect('/management'); - return; - } - - if (kfetchError.res.status !== 401 && kfetchError.res.status !== 403) { - toastNotifications.addDanger( - kfetchError.res.statusText || - this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.requestFailedErrorMessage', - defaultMessage: 'Request failed', - }) - ); - } - if (this.mounted) { - this.setState(() => ({ isLoading: false, jobs: [], total: 0 })); - } - return; - } - - if (this.mounted) { - this.setState(() => ({ - isLoading: false, - total, - jobs: jobs.map( - (job: JobQueueEntry): Job => { - const { _source: source } = job; - return { - id: job._id, - type: source.jobtype, - object_type: source.payload.objectType, - object_title: source.payload.title, - created_by: source.created_by, - created_at: source.created_at, - started_at: source.started_at, - completed_at: source.completed_at, - status: source.status, - statusLabel: jobStatusLabelsMap.get(source.status as JobStatuses) || source.status, - max_size_reached: source.output ? source.output.max_size_reached : false, - attempts: source.attempts, - max_attempts: source.max_attempts, - csv_contains_formulas: get(source, 'output.csv_contains_formulas'), - warnings: source.output ? source.output.warnings : undefined, - }; - } - ), - })); - } - }; - - private licenseAllowsToShowThisPage = () => { - return this.props.showLinks && this.props.enableLinks; - }; - - private formatDate(timestamp: string) { - try { - return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); - } catch (error) { - // ignore parse error and display unformatted value - return timestamp; - } - } -} - -export const ReportListing = injectI18n(ReportListingUi); diff --git a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx deleted file mode 100644 index aaf4021302a97..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component, ReactElement } from 'react'; -import { toastNotifications } from 'ui/notify'; -import url from 'url'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -import * as reportingClient from '../lib/reporting_client'; - -interface Props { - reportType: string; - layoutId: string | undefined; - objectId?: string; - objectType: string; - getJobParams: () => any; - options?: ReactElement; - isDirty: boolean; - onClose: () => void; - intl: InjectedIntl; -} - -interface State { - isStale: boolean; - absoluteUrl: string; - layoutId: string; -} - -class ReportingPanelContentUi extends Component { - public static getDerivedStateFromProps(nextProps: Props, prevState: State) { - if (nextProps.layoutId !== prevState.layoutId) { - return { - ...prevState, - absoluteUrl: ReportingPanelContentUi.getAbsoluteReportGenerationUrl(nextProps), - }; - } - return prevState; - } - - private static getAbsoluteReportGenerationUrl = (props: Props) => { - const relativePath = reportingClient.getReportingJobPath( - props.reportType, - props.getJobParams() - ); - return url.resolve(window.location.href, relativePath); - }; - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isStale: false, - absoluteUrl: '', - layoutId: '', - }; - } - - public componentWillUnmount() { - window.removeEventListener('hashchange', this.markAsStale); - window.removeEventListener('resize', this.setAbsoluteReportGenerationUrl); - - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - - window.addEventListener('hashchange', this.markAsStale, false); - window.addEventListener('resize', this.setAbsoluteReportGenerationUrl); - } - - public render() { - if (this.isNotSaved() || this.props.isDirty || this.state.isStale) { - return ( - - - } - > - {this.renderGenerateReportButton(true)} - - - ); - } - - const reportMsg = ( - - ); - - return ( - - -

{reportMsg}

-
- - - {this.props.options} - - {this.renderGenerateReportButton(false)} - - - -

- -

-
- - - - {copy => ( - - - - )} - -
- ); - } - - private renderGenerateReportButton = (isDisabled: boolean) => { - return ( - - - - ); - }; - - private prettyPrintReportingType = () => { - switch (this.props.reportType) { - case 'printablePdf': - return 'PDF'; - case 'csv': - return 'CSV'; - case 'png': - return 'PNG'; - default: - return this.props.reportType; - } - }; - - private markAsStale = () => { - if (!this.mounted) { - return; - } - - this.setState({ isStale: true }); - }; - - private isNotSaved = () => { - return this.props.objectId === undefined || this.props.objectId === ''; - }; - - private setAbsoluteReportGenerationUrl = () => { - if (!this.mounted) { - return; - } - const absoluteUrl = ReportingPanelContentUi.getAbsoluteReportGenerationUrl(this.props); - this.setState({ absoluteUrl }); - }; - - private createReportingJob = () => { - const { intl } = this.props; - - return reportingClient - .createReportingJob(this.props.reportType, this.props.getJobParams()) - .then(() => { - toastNotifications.addSuccess({ - title: intl.formatMessage( - { - id: 'xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle', - defaultMessage: 'Queued report for {objectType}', - }, - { objectType: this.props.objectType } - ), - text: toMountPoint( - - ), - 'data-test-subj': 'queueReportSuccess', - }); - this.props.onClose(); - }) - .catch((error: any) => { - if (error.message === 'not exportable') { - return toastNotifications.addWarning({ - title: intl.formatMessage( - { - id: 'xpack.reporting.panelContent.whatCanBeExportedWarningTitle', - defaultMessage: 'Only saved {objectType} can be exported', - }, - { objectType: this.props.objectType } - ), - text: toMountPoint( - - ), - }); - } - - const defaultMessage = - error?.res?.status === 403 ? ( - - ) : ( - - ); - - toastNotifications.addDanger({ - title: intl.formatMessage({ - id: 'xpack.reporting.panelContent.notification.reportingErrorTitle', - defaultMessage: 'Reporting error', - }), - text: toMountPoint(error.message || defaultMessage), - 'data-test-subj': 'queueReportError', - }); - }); - }; -} - -export const ReportingPanelContent = injectI18n(ReportingPanelContentUi); diff --git a/x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx deleted file mode 100644 index cf6bb94876361..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Component, Fragment } from 'react'; -import { ReportingPanelContent } from './reporting_panel_content'; - -interface Props { - reportType: string; - objectId?: string; - objectType: string; - getJobParams: () => any; - isDirty: boolean; - onClose: () => void; -} - -interface State { - isPreserveLayoutSupported: boolean; - usePrintLayout: boolean; -} - -export class ScreenCapturePanelContent extends Component { - constructor(props: Props) { - super(props); - - const isPreserveLayoutSupported = - props.reportType !== 'png' && props.objectType !== 'visualization'; - this.state = { - isPreserveLayoutSupported, - usePrintLayout: false, - }; - } - - public render() { - return ( - - ); - } - - private renderOptions = () => { - if (this.state.isPreserveLayoutSupported) { - return ( - - - } - checked={this.state.usePrintLayout} - onChange={this.handlePrintLayoutChange} - data-test-subj="usePrintLayout" - /> - - - ); - } - - return ( - - - - ); - }; - - private handlePrintLayoutChange = (evt: any) => { - this.setState({ usePrintLayout: evt.target.checked }); - }; - - private getLayout = () => { - if (this.state.usePrintLayout) { - return { id: 'print' }; - } - - const el = document.querySelector('[data-shared-items-container]'); - const bounds = el ? el.getBoundingClientRect() : { height: 768, width: 1024 }; - - return { - id: this.props.reportType === 'png' ? 'png' : 'preserve_layout', - dimensions: { - height: bounds.height, - width: bounds.width, - }, - }; - }; - - private getJobParams = () => { - return { - ...this.props.getJobParams(), - layout: this.getLayout(), - }; - }; -} diff --git a/x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx b/x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx deleted file mode 100644 index 29c51217a5c64..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum JobStatuses { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} diff --git a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts b/x-pack/legacy/plugins/reporting/public/lib/download_report.ts deleted file mode 100644 index 54194c87afabc..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npStart } from 'ui/new_platform'; -import { API_BASE_URL } from '../../common/constants'; - -const { core } = npStart; - -export function getReportURL(jobId: string) { - const apiBaseUrl = core.http.basePath.prepend(API_BASE_URL); - const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`; - - return downloadLink; -} - -export function downloadReport(jobId: string) { - const location = getReportURL(jobId); - - window.open(location); -} diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts deleted file mode 100644 index 3a61bc1e5a044..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants'; - -type jobId = string; - -const set = (jobs: any) => { - sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs)); -}; - -const getAll = () => { - const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); - return sessionValue ? JSON.parse(sessionValue) : []; -}; - -export const add = (jobId: jobId) => { - const jobs = getAll(); - jobs.push(jobId); - set(jobs); -}; - -export const remove = (jobId: jobId) => { - const jobs = getAll(); - const index = jobs.indexOf(jobId); - - if (!index) { - throw new Error('Unable to find job to remove it'); - } - - jobs.splice(index, 1); - set(jobs); -}; diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts deleted file mode 100644 index 87d4174168b7f..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npStart } from 'ui/new_platform'; -import { API_LIST_URL } from '../../common/constants'; - -const { core } = npStart; - -export interface JobQueueEntry { - _id: string; - _source: any; -} - -export interface JobContent { - content: string; - content_type: boolean; -} - -export interface JobInfo { - kibana_name: string; - kibana_id: string; - browser_type: string; - created_at: string; - priority: number; - jobtype: string; - created_by: string; - timeout: number; - output: { - content_type: string; - size: number; - warnings: string[]; - }; - process_expiration: string; - completed_at: string; - payload: { - layout: { id: string; dimensions: { width: number; height: number } }; - objects: Array<{ relativeUrl: string }>; - type: string; - title: string; - forceNow: string; - browserTimezone: string; - }; - meta: { - layout: string; - objectType: string; - }; - max_attempts: number; - started_at: string; - attempts: number; - status: string; -} - -class JobQueueClient { - public list = (page = 0, jobIds: string[] = []): Promise => { - const query = { page } as any; - if (jobIds.length > 0) { - // Only getting the first 10, to prevent URL overflows - query.ids = jobIds.slice(0, 10).join(','); - } - - return core.http.get(`${API_LIST_URL}/list`, { - query, - asSystemRequest: true, - }); - }; - - public total(): Promise { - return core.http.get(`${API_LIST_URL}/count`, { - asSystemRequest: true, - }); - } - - public getContent(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/output/${jobId}`, { - asSystemRequest: true, - }); - } - - public getInfo(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/info/${jobId}`, { - asSystemRequest: true, - }); - } -} - -export const jobQueueClient = new JobQueueClient(); diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts deleted file mode 100644 index d471dc57fc9e1..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { stringify } from 'query-string'; -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import rison from 'rison-node'; -import { add } from './job_completion_notifications'; - -const { core } = npStart; -const API_BASE_URL = '/api/reporting/generate'; - -interface JobParams { - [paramName: string]: any; -} - -export const getReportingJobPath = (exportType: string, jobParams: JobParams) => { - const params = stringify({ jobParams: rison.encode(jobParams) }); - - return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; -}; - -export const createReportingJob = async (exportType: string, jobParams: any) => { - const jobParamsRison = rison.encode(jobParams); - const resp = await core.http.post(`${API_BASE_URL}/${exportType}`, { - method: 'POST', - body: JSON.stringify({ - jobParams: jobParamsRison, - }), - }); - - add(resp.job.id); - - return resp; -}; diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx deleted file mode 100644 index 4c9cd890ee75b..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import dateMath from '@elastic/datemath'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; - -import { npSetup, npStart } from 'ui/new_platform'; -import { - ActionByType, - IncompatibleActionError, -} from '../../../../../../src/plugins/ui_actions/public'; - -import { - ViewMode, - IEmbeddable, - CONTEXT_MENU_TRIGGER, -} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; -import { ISearchEmbeddable } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types'; - -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; - -const { core } = npStart; - -function isSavedSearchEmbeddable( - embeddable: IEmbeddable | ISearchEmbeddable -): embeddable is ISearchEmbeddable { - return embeddable.type === SEARCH_EMBEDDABLE_TYPE; -} - -export interface CSVActionContext { - embeddable: ISearchEmbeddable; -} - -declare module '../../../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [CSV_REPORTING_ACTION]: CSVActionContext; - } -} - -class GetCsvReportPanelAction implements ActionByType { - private isDownloading: boolean; - public readonly type = CSV_REPORTING_ACTION; - public readonly id = CSV_REPORTING_ACTION; - - constructor() { - this.isDownloading = false; - } - - public getIconType() { - return 'document'; - } - - public getDisplayName() { - return i18n.translate('xpack.reporting.dashboard.downloadCsvPanelTitle', { - defaultMessage: 'Download CSV', - }); - } - - public async getSearchRequestBody({ searchEmbeddable }: { searchEmbeddable: any }) { - const adapters = searchEmbeddable.getInspectorAdapters(); - if (!adapters) { - return {}; - } - - if (adapters.requests.requests.length === 0) { - return {}; - } - - return searchEmbeddable.getSavedSearch().searchSource.getSearchRequestBody(); - } - - public isCompatible = async (context: CSVActionContext) => { - const { embeddable } = context; - - return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; - }; - - public execute = async (context: CSVActionContext) => { - const { embeddable } = context; - - if (!isSavedSearchEmbeddable(embeddable)) { - throw new IncompatibleActionError(); - } - - if (this.isDownloading) { - return; - } - - const { - timeRange: { to, from }, - } = embeddable.getInput(); - - const searchEmbeddable = embeddable; - const searchRequestBody = await this.getSearchRequestBody({ searchEmbeddable }); - const state = _.pick(searchRequestBody, ['sort', 'docvalue_fields', 'query']); - const kibanaTimezone = core.uiSettings.get('dateFormat:tz'); - - const id = `search:${embeddable.getSavedSearch().id}`; - const filename = embeddable.getTitle(); - const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; - const fromTime = dateMath.parse(from); - const toTime = dateMath.parse(to); - - if (!fromTime || !toTime) { - return this.onGenerationFail( - new Error(`Invalid time range: From: ${fromTime}, To: ${toTime}`) - ); - } - - const body = JSON.stringify({ - timerange: { - min: fromTime.format(), - max: toTime.format(), - timezone, - }, - state, - }); - - this.isDownloading = true; - - core.notifications.toasts.addSuccess({ - title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', { - defaultMessage: `CSV Download Started`, - }), - text: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedMessage', { - defaultMessage: `Your CSV will download momentarily.`, - }), - 'data-test-subj': 'csvDownloadStarted', - }); - - await core.http - .post(`${API_GENERATE_IMMEDIATE}/${id}`, { body }) - .then((rawResponse: string) => { - this.isDownloading = false; - - const download = `${filename}.csv`; - const blob = new Blob([rawResponse], { type: 'text/csv;charset=utf-8;' }); - - // Hack for IE11 Support - if (window.navigator.msSaveOrOpenBlob) { - return window.navigator.msSaveOrOpenBlob(blob, download); - } - - const a = window.document.createElement('a'); - const downloadObject = window.URL.createObjectURL(blob); - - a.href = downloadObject; - a.download = download; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(downloadObject); - document.body.removeChild(a); - }) - .catch(this.onGenerationFail.bind(this)); - }; - - private onGenerationFail(error: Error) { - this.isDownloading = false; - core.notifications.toasts.addDanger({ - title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', { - defaultMessage: `CSV download failed`, - }), - text: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadMessage', { - defaultMessage: `We couldn't generate your CSV at this time.`, - }), - 'data-test-subj': 'downloadCsvFail', - }); - } -} - -const action = new GetCsvReportPanelAction(); - -npSetup.plugins.uiActions.registerAction(action); -npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); diff --git a/x-pack/legacy/plugins/reporting/public/register_feature.ts b/x-pack/legacy/plugins/reporting/public/register_feature.ts deleted file mode 100644 index 4e8d32facfcec..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/register_feature.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'reporting', - title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { - defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', - }), - icon: 'reportingApp', - path: '/app/kibana#/management/kibana/reporting', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, -}); diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx deleted file mode 100644 index 3c9d1d7262587..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -// @ts-ignore: implicit any for JS file -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import React from 'react'; -import { npSetup } from 'ui/new_platform'; -import { ReportingPanelContent } from '../components/reporting_panel_content'; -import { ShareContext } from '../../../../../../src/plugins/share/public'; - -function reportingProvider() { - const getShareMenuItems = ({ - objectType, - objectId, - sharingData, - isDirty, - onClose, - }: ShareContext) => { - if ('search' !== objectType) { - return []; - } - - const getJobParams = () => { - return { - ...sharingData, - type: objectType, - }; - }; - - const shareActions = []; - if (xpackInfo.get('features.reporting.csv.showLinks', false)) { - const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.csvReportsButtonLabel', { - defaultMessage: 'CSV Reports', - }); - - shareActions.push({ - shareMenuItem: { - name: panelTitle, - icon: 'document', - toolTipContent: xpackInfo.get('features.reporting.csv.message'), - disabled: !xpackInfo.get('features.reporting.csv.enableLinks', false) ? true : false, - ['data-test-subj']: 'csvReportMenuItem', - sortOrder: 1, - }, - panel: { - id: 'csvReportingPanel', - title: panelTitle, - content: ( - - ), - }, - }); - } - - return shareActions; - }; - - return { - id: 'csvReports', - getShareMenuItems, - }; -} - -npSetup.plugins.share.register(reportingProvider()); diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx deleted file mode 100644 index 4153c7cdbdb0b..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; -// @ts-ignore: implicit any for JS file -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { npSetup, npStart } from 'ui/new_platform'; -import React from 'react'; -import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; -import { ShareContext } from '../../../../../../src/plugins/share/public'; - -const { core } = npSetup; - -async function reportingProvider() { - const getShareMenuItems = ({ - objectType, - objectId, - sharingData, - isDirty, - onClose, - shareableUrl, - }: ShareContext) => { - if (!['dashboard', 'visualization'].includes(objectType)) { - return []; - } - // Dashboard only mode does not currently support reporting - // https://github.com/elastic/kibana/issues/18286 - if ( - objectType === 'dashboard' && - npStart.plugins.kibanaLegacy.dashboardConfig.getHideWriteControls() - ) { - return []; - } - - const getReportingJobParams = () => { - // Replace hashes with original RISON values. - const relativeUrl = shareableUrl.replace( - window.location.origin + core.http.basePath.get(), - '' - ); - - const browserTimezone = - core.uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : core.uiSettings.get('dateFormat:tz'); - - return { - ...sharingData, - objectType, - browserTimezone, - relativeUrls: [relativeUrl], - }; - }; - - const getPngJobParams = () => { - // Replace hashes with original RISON values. - const relativeUrl = shareableUrl.replace( - window.location.origin + core.http.basePath.get(), - '' - ); - - const browserTimezone = - core.uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : core.uiSettings.get('dateFormat:tz'); - - return { - ...sharingData, - objectType, - browserTimezone, - relativeUrl, - }; - }; - - const shareActions = []; - if (xpackInfo.get('features.reporting.printablePdf.showLinks', false)) { - const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.pdfReportsButtonLabel', { - defaultMessage: 'PDF Reports', - }); - - shareActions.push({ - shareMenuItem: { - name: panelTitle, - icon: 'document', - toolTipContent: xpackInfo.get('features.reporting.printablePdf.message'), - disabled: !xpackInfo.get('features.reporting.printablePdf.enableLinks', false) - ? true - : false, - ['data-test-subj']: 'pdfReportMenuItem', - sortOrder: 10, - }, - panel: { - id: 'reportingPdfPanel', - title: panelTitle, - content: ( - - ), - }, - }); - } - - if (xpackInfo.get('features.reporting.png.showLinks', false)) { - const panelTitle = 'PNG Reports'; - - shareActions.push({ - shareMenuItem: { - name: panelTitle, - icon: 'document', - toolTipContent: xpackInfo.get('features.reporting.png.message'), - disabled: !xpackInfo.get('features.reporting.png.enableLinks', false) ? true : false, - ['data-test-subj']: 'pngReportMenuItem', - sortOrder: 10, - }, - panel: { - id: 'reportingPngPanel', - title: panelTitle, - content: ( - - ), - }, - }); - } - - return shareActions; - }; - - return { - id: 'screenCaptureReports', - getShareMenuItems, - }; -} - -(async () => { - npSetup.plugins.share.register(await reportingProvider()); -})(); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/index.js b/x-pack/legacy/plugins/reporting/public/views/management/index.js deleted file mode 100644 index 0ed6fe09ef80a..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import './management'; diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html b/x-pack/legacy/plugins/reporting/public/views/management/jobs.html deleted file mode 100644 index 5471513d64d95..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
\ No newline at end of file diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js b/x-pack/legacy/plugins/reporting/public/views/management/jobs.js deleted file mode 100644 index 7205fad8cca53..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import routes from 'ui/routes'; -import template from 'plugins/reporting/views/management/jobs.html'; - -import { ReportListing } from '../../components/report_listing'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; - -const REACT_ANCHOR_DOM_ELEMENT_ID = 'reportListingAnchor'; - -routes.when('/management/kibana/reporting', { - template, - k7Breadcrumbs: () => [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.reporting.breadcrumb', { - defaultMessage: 'Reporting', - }), - }, - ], - controllerAs: 'jobsCtrl', - controller($scope, kbnUrl) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); - - $scope.$on('$destroy', () => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (node) { - unmountComponentAtNode(node); - } - }); - }, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/management.js b/x-pack/legacy/plugins/reporting/public/views/management/management.js deleted file mode 100644 index 8643e6fa8b8b4..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/management.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import 'plugins/reporting/views/management/jobs'; - -routes.defaults(/\/management/, { - resolve: { - reportingManagementSection: function() { - const kibanaManagementSection = management.getSection('kibana'); - const showReportingLinks = xpackInfo.get('features.reporting.management.showLinks'); - - kibanaManagementSection.deregister('reporting'); - if (showReportingLinks) { - const enableReportingLinks = xpackInfo.get('features.reporting.management.enableLinks'); - const tooltipMessage = xpackInfo.get('features.reporting.management.message'); - - let url; - let tooltip; - if (enableReportingLinks) { - url = '#/management/kibana/reporting'; - } else { - tooltip = tooltipMessage; - } - - return kibanaManagementSection.register('reporting', { - order: 15, - display: i18n.translate('xpack.reporting.management.reportingTitle', { - defaultMessage: 'Reporting', - }), - url, - tooltip, - }); - } - }, - }, -}); diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 4ed31130c3ed1..7e70f68096fbe 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -6,9 +6,11 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { ReportingPublicPlugin } from './plugin'; +import * as jobCompletionNotifications from './lib/job_completion_notifications'; export function plugin(initializerContext: PluginInitializerContext) { return new ReportingPublicPlugin(initializerContext); } export { ReportingPublicPlugin as Plugin }; +export { jobCompletionNotifications }; From ed82044f53f3079b01f66e574818b2aac0c09873 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 9 Mar 2020 10:25:00 -0700 Subject: [PATCH 09/23] Fixing stream-client for new platform APIs --- .../__snapshots__/stream_handler.test.ts.snap | 4 +- .../public/lib/stream_handler.test.ts | 64 +++++-------------- 2 files changed, 19 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap index 94dad950a6b56..6b95a00ea0009 100644 --- a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap +++ b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap @@ -95,7 +95,9 @@ Array [ id="xpack.reporting.publicNotifier.error.checkManagement" values={ Object { - "path": + "path": { + findForJobIds: async (jobIds: string[]) => { return mockJobsFound as SourceJob[]; }, - getContent: () => { - return Promise.resolve('this is the completed report data'); + getContent: (): Promise => { + return Promise.resolve({ content: 'this is the completed report data' }); }, -}; - -const httpMock: HttpService = ({ - basePath: { - prepend: stub(), - }, -} as unknown) as HttpSetup; + getManagementLink: () => '/#management', + getDownloadLink: () => '/reporting/download/job-123', +} as any; const mockShowDanger = stub(); const mockShowSuccess = stub(); @@ -76,17 +72,13 @@ describe('stream handler', () => { }); it('constructs', () => { - const sh = new ReportingNotifierStreamHandler(httpMock, notificationsMock, jobQueueClientMock); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); expect(sh).not.toBe(null); }); describe('findChangedStatusJobs', () => { it('finds no changed status jobs from empty', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); const findJobs = sh.findChangedStatusJobs([]); findJobs.subscribe(data => { expect(data).toEqual({ completed: [], failed: [] }); @@ -95,11 +87,7 @@ describe('stream handler', () => { }); it('finds changed status jobs', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); const findJobs = sh.findChangedStatusJobs([ 'job-source-mock1', 'job-source-mock2', @@ -115,11 +103,7 @@ describe('stream handler', () => { describe('showNotifications', () => { it('show success', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { @@ -140,11 +124,7 @@ describe('stream handler', () => { }); it('show max length warning', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { @@ -166,11 +146,7 @@ describe('stream handler', () => { }); it('show csv formulas warning', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { @@ -192,11 +168,7 @@ describe('stream handler', () => { }); it('show failed job toast', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [], failed: [ @@ -217,11 +189,7 @@ describe('stream handler', () => { }); it('show multiple toast', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { From de45fd9b45124f2984cd5658ad51f511b26ff608 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 9 Mar 2020 11:54:24 -0700 Subject: [PATCH 10/23] Fixing types and tests --- x-pack/plugins/reporting/constants.ts | 2 +- .../reporting/public/panel_actions/get_csv_panel_action.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 843ba23e27e44..8f47a0a6b2ac1 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -18,7 +18,7 @@ export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { export const API_BASE_URL = '/api/reporting'; export const API_LIST_URL = `${API_BASE_URL}/jobs`; export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_GENERATE_IMMEDIATE = `${API_BASE_GENERATE}/v1/generate/immediate/csv/saved-object`; +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporting'; // Statuses diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 1bfccf1f135df..37fe36ccfe5f3 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -33,7 +33,7 @@ interface ActionContext { export class GetCsvReportPanelAction implements Action { private isDownloading: boolean; - public readonly type = CSV_REPORTING_ACTION; + public readonly type = ''; public readonly id = CSV_REPORTING_ACTION; private core: CoreSetup; From 257d9c62334873f03f5bcdfcb9e3d4d5ff696391 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 9 Mar 2020 15:28:00 -0700 Subject: [PATCH 11/23] Fix broken mock --- .../reporting/public/components/report_info_button.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx index 07ffb1b77433e..2edd59e6de7a3 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx @@ -9,7 +9,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportInfoButton } from './report_info_button'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -jest.mock('../lib/job_queue_client'); +jest.mock('../lib/reporting_api_client'); const httpSetup = {} as any; const apiClient = new ReportingAPIClient(httpSetup); From 7dee2d06b704ed1342b029f3447ee733de7d5746 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Tue, 10 Mar 2020 10:03:51 -0700 Subject: [PATCH 12/23] Adds back in warnings to report info button --- .../reporting/public/components/report_info_button.tsx | 8 ++++++++ .../plugins/reporting/public/lib/reporting_api_client.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx index 4026743fe7bc8..24405876fbeaf 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -87,6 +87,7 @@ export class ReportInfoButton extends Component { const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; const priority = info.priority ? info.priority.toString() : NA; const timeout = info.timeout ? info.timeout.toString() : NA; + const warnings = info.output && info.output.warnings ? info.output.warnings.join(',') : null; const jobInfoParts: JobInfoMap = { datetimes: [ @@ -178,6 +179,13 @@ export class ReportInfoButton extends Component { ], }; + if (warnings) { + jobInfoParts.status.push({ + title: 'Errors', + description: warnings, + }); + } + return ( Date: Tue, 10 Mar 2020 10:04:56 -0700 Subject: [PATCH 13/23] kibana.json line-breaks on required plugins --- x-pack/plugins/reporting/kibana.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 61ddbd58ba3c5..a7e2bd288f0b1 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,7 +2,15 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["home", "management", "licensing", "uiActions", "embeddable", "share", "kibanaLegacy"], + "requiredPlugins": [ + "home", + "management", + "licensing", + "uiActions", + "embeddable", + "share", + "kibanaLegacy" + ], "server": false, "ui": true } From 1f07d5b81945807817ee3e0705bf3954b2063eee Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Tue, 10 Mar 2020 14:12:07 -0700 Subject: [PATCH 14/23] Fixing broked snapshots --- .../report_info_button.test.tsx.snap | 96 ++++++++++++------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap index 2055afdcf2bfe..f89e90cc4860c 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap @@ -182,9 +182,13 @@ Array [ class="euiFlyoutBody__overflow" >
- Could not fetch the job info +
+ Could not fetch the job info +
@@ -243,9 +247,13 @@ Array [ class="euiFlyoutBody__overflow" >
- Could not fetch the job info +
+ Could not fetch the job info +
@@ -332,13 +340,17 @@ Array [
- -
- Could not fetch the job info -
-
+
+ +
+ Could not fetch the job info +
+
+
@@ -440,13 +452,17 @@ Array [
- -
- Could not fetch the job info -
-
+
+ +
+ Could not fetch the job info +
+
+
@@ -599,8 +615,12 @@ Array [ class="euiFlyoutBody__overflow" >
+ class="euiFlyoutBody__overflowContent" + > +
+
@@ -658,8 +678,12 @@ Array [ class="euiFlyoutBody__overflow" >
+ class="euiFlyoutBody__overflowContent" + > +
+
@@ -745,11 +769,15 @@ Array [
- -
- +
+ +
+ +
@@ -851,11 +879,15 @@ Array [
- -
- +
+ +
+ +
From 15a2fbf0e175a62c8b9fe78ed94ebf5a596d5276 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 12 Mar 2020 15:46:05 -0700 Subject: [PATCH 15/23] Fix license checks in client-side components --- x-pack/plugins/reporting/index.d.ts | 6 +++ .../public/components/report_listing.tsx | 9 ++-- .../public/lib/license_check.test.ts | 50 ++++++++++++++++++ .../reporting/public/lib/license_check.ts | 52 +++++++++++++++++++ .../register_csv_reporting.tsx | 8 +-- .../register_pdf_png_reporting.tsx | 8 +-- 6 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/reporting/public/lib/license_check.test.ts create mode 100644 x-pack/plugins/reporting/public/lib/license_check.ts diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 3b83a7b3d1251..dcdb3b6c0839f 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -69,3 +69,9 @@ export interface PollerOptions { successFunction?: (...args: any) => any; errorFunction?: (error: Error) => any; } + +export interface LicenseCheckResults { + enableLinks: boolean; + showLinks: boolean; + message: string; +} diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index a6710af7ccdb1..9ba4c4ca8a2cf 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -27,6 +27,7 @@ import { LicensingPluginSetup, LICENSE_CHECK_STATE, ILicense } from '../../../li import { Poller } from '../../common/poller'; import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; import { ReportingAPIClient, JobQueueEntry } from '../lib/reporting_api_client'; +import { checkLicense } from '../lib/license_check'; import { ReportErrorButton } from './report_error_button'; import { ReportInfoButton } from './report_info_button'; @@ -169,14 +170,14 @@ class ReportListingUi extends Component { } private licenseHandler = (license: ILicense) => { - const { state, message } = license.check('reporting', 'basic'); - const enableLinks = state === LICENSE_CHECK_STATE.Valid; - const showLinks = enableLinks; + const { enableLinks, showLinks, message: badLicenseMessage } = checkLicense( + license.check('reporting', 'basic') + ); this.setState({ enableLinks, showLinks, - badLicenseMessage: message || '', + badLicenseMessage, }); }; diff --git a/x-pack/plugins/reporting/public/lib/license_check.test.ts b/x-pack/plugins/reporting/public/lib/license_check.test.ts new file mode 100644 index 0000000000000..24e14969d2c81 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/license_check.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { checkLicense } from './license_check'; +import { LicenseCheck } from '../../../licensing/public'; + +describe('License check', () => { + it('enables and shows links when licenses are good mkay', () => { + expect(checkLicense({ state: 'VALID' } as LicenseCheck)).toEqual({ + enableLinks: true, + showLinks: true, + message: '', + }); + }); + + it('disables and shows links when licenses are not valid', () => { + expect(checkLicense({ state: 'INVALID' } as LicenseCheck)).toEqual({ + enableLinks: false, + showLinks: false, + message: 'Your license does not support Reporting. Please upgrade your license.', + }); + }); + + it('shows links, but disables them, on expired licenses', () => { + expect(checkLicense({ state: 'EXPIRED' } as LicenseCheck)).toEqual({ + enableLinks: false, + showLinks: true, + message: 'You cannot use Reporting because your license has expired.', + }); + }); + + it('shows links, but disables them, when license checks are unavailable', () => { + expect(checkLicense({ state: 'UNAVAILABLE' } as LicenseCheck)).toEqual({ + enableLinks: false, + showLinks: true, + message: + 'You cannot use Reporting because license information is not available at this time.', + }); + }); + + it('shows and enables links if state is not known', () => { + expect(checkLicense({ state: 'PONYFOO' } as any)).toEqual({ + enableLinks: true, + showLinks: true, + message: '', + }); + }); +}); diff --git a/x-pack/plugins/reporting/public/lib/license_check.ts b/x-pack/plugins/reporting/public/lib/license_check.ts new file mode 100644 index 0000000000000..ca803fb38ef2a --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/license_check.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseCheckResults } from '../..'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../licensing/public'; + +export const checkLicense = (checkResults: LicenseCheck): LicenseCheckResults => { + switch (checkResults.state) { + case LICENSE_CHECK_STATE.Valid: { + return { + showLinks: true, + enableLinks: true, + message: '', + }; + } + + case LICENSE_CHECK_STATE.Invalid: { + return { + showLinks: false, + enableLinks: false, + message: 'Your license does not support Reporting. Please upgrade your license.', + }; + } + + case LICENSE_CHECK_STATE.Unavailable: { + return { + showLinks: true, + enableLinks: false, + message: + 'You cannot use Reporting because license information is not available at this time.', + }; + } + + case LICENSE_CHECK_STATE.Expired: { + return { + showLinks: true, + enableLinks: false, + message: 'You cannot use Reporting because your license has expired.', + }; + } + + default: { + return { + showLinks: true, + enableLinks: true, + message: '', + }; + } + } +}; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index eee3c4da1cb47..8f544c091b79c 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { ReportingPanelContent } from '../components/reporting_panel_content'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../../../licensing/public'; +import { checkLicense } from '../lib/license_check'; +import { LicensingPluginSetup } from '../../../licensing/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { ToastsSetup } from '../..'; @@ -25,11 +26,10 @@ export const csvReportingProvider = ({ apiClient, toasts, license$ }: ReportingP let hasCSVReporting = false; license$.subscribe(license => { - const { state, message = '' } = license.check('reporting', 'basic'); - const enableLinks = state === LICENSE_CHECK_STATE.Valid; + const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'basic')); toolTipContent = message; - hasCSVReporting = enableLinks; + hasCSVReporting = showLinks; disabled = !enableLinks; }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 04c804c96399a..0bccea385bd4c 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -8,8 +8,9 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { checkLicense } from '../lib/license_check'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; -import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../../../licensing/public'; +import { LicensingPluginSetup } from '../../../licensing/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { ToastsSetup, IUiSettingsClient } from '../../'; @@ -31,11 +32,10 @@ export const reportingPDFPNGProvider = ({ let hasPDFPNGReporting = false; license$.subscribe(license => { - const { state, message = '' } = license.check('reporting', 'gold'); - const enableLinks = state === LICENSE_CHECK_STATE.Valid; + const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'gold')); toolTipContent = message; - hasPDFPNGReporting = enableLinks; + hasPDFPNGReporting = showLinks; disabled = !enableLinks; }); From 3008ad7e4afc3d91ac1fb60c8439d6dda7930f99 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 13 Mar 2020 08:55:37 -0700 Subject: [PATCH 16/23] Adding back in warnings to report_listing component --- .../public/components/report_listing.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 9ba4c4ca8a2cf..194298a30adaa 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -46,6 +46,7 @@ interface Job { attempts: number; max_attempts: number; csv_contains_formulas: boolean; + warnings: string[]; } interface Props { @@ -231,7 +232,7 @@ class ReportListingUi extends Component { return (
@@ -243,13 +244,27 @@ class ReportListingUi extends Component { maxSizeReached = ( ); } + let warnings; + if (record.warnings) { + warnings = ( + + + + + + ); + } + let statusTimestamp; if (status === JobStatuses.PROCESSING && record.started_at) { statusTimestamp = this.formatDate(record.started_at); @@ -465,6 +480,7 @@ class ReportListingUi extends Component { attempts: source.attempts, max_attempts: source.max_attempts, csv_contains_formulas: get(source, 'output.csv_contains_formulas'), + warnings: source.output ? source.output.warnings : undefined, }; } ), From 5f2bb0cb3a88dae3fd1d942873a8b31502c9214a Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 13 Mar 2020 08:56:29 -0700 Subject: [PATCH 17/23] Fix danglig unused import --- x-pack/plugins/reporting/public/components/report_listing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 194298a30adaa..9c618af139e78 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import { ToastsSetup, ApplicationStart } from '../../'; -import { LicensingPluginSetup, LICENSE_CHECK_STATE, ILicense } from '../../../licensing/public'; +import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; import { Poller } from '../../common/poller'; import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; import { ReportingAPIClient, JobQueueEntry } from '../lib/reporting_api_client'; From d3d668ceb8bd412d393992598ee5c75d416bf9e6 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 13 Mar 2020 09:21:31 -0700 Subject: [PATCH 18/23] Adds license checks for basic to our csv panel action --- x-pack/legacy/plugins/reporting/index.ts | 9 +-------- x-pack/legacy/plugins/reporting/types.d.ts | 16 ---------------- .../panel_actions/get_csv_panel_action.tsx | 17 +++++++++++++++-- x-pack/plugins/reporting/public/plugin.tsx | 2 +- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index c3ec6c4886f04..89e98302cddc9 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -10,7 +10,7 @@ import { resolve } from 'path'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; -import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types'; +import { ReportingPluginSpecOptions } from './types'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); @@ -25,13 +25,6 @@ export const reporting = (kibana: any) => { config: reportingConfig, uiExports: { - injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { - const config = server.config(); - return { - reportingPollConfig: options ? options.poll : {}, - enablePanelActionDownload: config.get('xpack.reporting.csv.enablePanelActionDownload'), - }; - }, uiSettingDefaults: { [UI_SETTINGS_CUSTOM_PDF_LOGO]: { name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index b4d49fd21f230..917e9d7daae40 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -23,22 +23,6 @@ export type Job = EventEmitter & { }; }; -export interface ReportingConfigOptions { - browser: BrowserConfig; - poll: { - jobCompletionNotifier: { - interval: number; - intervalErrorMultiplier: number; - }; - jobsRefresh: { - interval: number; - intervalErrorMultiplier: number; - }; - }; - queue: QueueConfig; - capture: CaptureConfig; -} - export interface NetworkPolicyRule { allow: boolean; protocol: string; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 37fe36ccfe5f3..7a1fbd49f9f7e 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,6 +6,8 @@ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; +import { LicensingPluginSetup } from '../../../licensing/public'; +import { checkLicense } from '../lib/license_check'; import { CoreSetup } from '../../../../../src/core/public'; import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; @@ -15,7 +17,7 @@ import { IEmbeddable, } from '../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -// @TODO: These will probably relocate at some point, and will need to be fixed +// @TODO: These import paths will need to be updated once discovery moves to non-legacy dir import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; import { ISearchEmbeddable } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types'; @@ -35,11 +37,18 @@ export class GetCsvReportPanelAction implements Action { private isDownloading: boolean; public readonly type = ''; public readonly id = CSV_REPORTING_ACTION; + private canDownloadCSV: boolean = false; private core: CoreSetup; - constructor(core: CoreSetup) { + constructor(core: CoreSetup, license$: LicensingPluginSetup['license$']) { this.isDownloading = false; this.core = core; + + license$.subscribe(license => { + const results = license.check('reporting', 'basic'); + const { showLinks } = checkLicense(results); + this.canDownloadCSV = showLinks; + }); } public getIconType() { @@ -66,6 +75,10 @@ export class GetCsvReportPanelAction implements Action { } public isCompatible = async (context: ActionContext) => { + if (!this.canDownloadCSV) { + return false; + } + const { embeddable } = context; return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index bced9d6ddd32a..6454c3de9e655 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -108,7 +108,7 @@ export class ReportingPublicPlugin implements Plugin { const { license$ } = licensing; const apiClient = new ReportingAPIClient(http); - const action = new GetCsvReportPanelAction(core); + const action = new GetCsvReportPanelAction(core, license$); home.featureCatalogue.register({ id: 'reporting', From a10e111d6ef9ae01ff43bda686414e570a7702cf Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 13 Mar 2020 11:22:06 -0700 Subject: [PATCH 19/23] Fixes issues from prior fork --- .../public/components/report_info_button.tsx | 180 +++++++++--------- .../public/components/report_listing.tsx | 3 +- 2 files changed, 92 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx index 24405876fbeaf..81a5af3b87957 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -89,103 +89,103 @@ export class ReportInfoButton extends Component { const timeout = info.timeout ? info.timeout.toString() : NA; const warnings = info.output && info.output.warnings ? info.output.warnings.join(',') : null; - const jobInfoParts: JobInfoMap = { - datetimes: [ - { - title: 'Created By', - description: info.created_by || NA, - }, - { - title: 'Created At', - description: info.created_at || NA, - }, - { - title: 'Started At', - description: info.started_at || NA, - }, - { - title: 'Completed At', - description: info.completed_at || NA, - }, - { - title: 'Processed By', - description: - info.kibana_name && info.kibana_id - ? `${info.kibana_name} (${info.kibana_id})` - : UNKNOWN, - }, - { - title: 'Browser Timezone', - description: get(info, 'payload.browserTimezone') || NA, - }, - ], - payload: [ - { - title: 'Title', - description: get(info, 'payload.title') || NA, - }, - { - title: 'Type', - description: get(info, 'payload.type') || NA, - }, - { - title: 'Layout', - description: get(info, 'meta.layout') || NA, - }, - { - title: 'Dimensions', - description: getDimensions(info), - }, - { - title: 'Job Type', - description: jobType, - }, - { - title: 'Content Type', - description: get(info, 'output.content_type') || NA, - }, - { - title: 'Size in Bytes', - description: get(info, 'output.size') || NA, - }, - ], - status: [ - { - title: 'Attempts', - description: attempts, - }, - { - title: 'Max Attempts', - description: maxAttempts, - }, - { - title: 'Priority', - description: priority, - }, - { - title: 'Timeout', - description: timeout, - }, - { - title: 'Status', - description: info.status || NA, - }, - { - title: 'Browser Type', - description: USES_HEADLESS_JOB_TYPES.includes(jobType) - ? info.browser_type || UNKNOWN - : NA, - }, - ], - }; + const jobInfoDateTimes: JobInfo[] = [ + { + title: 'Created By', + description: info.created_by || NA, + }, + { + title: 'Created At', + description: info.created_at || NA, + }, + { + title: 'Started At', + description: info.started_at || NA, + }, + { + title: 'Completed At', + description: info.completed_at || NA, + }, + { + title: 'Processed By', + description: + info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : UNKNOWN, + }, + { + title: 'Browser Timezone', + description: get(info, 'payload.browserTimezone') || NA, + }, + ]; + const jobInfoPayload: JobInfo[] = [ + { + title: 'Title', + description: get(info, 'payload.title') || NA, + }, + { + title: 'Type', + description: get(info, 'payload.type') || NA, + }, + { + title: 'Layout', + description: get(info, 'meta.layout') || NA, + }, + { + title: 'Dimensions', + description: getDimensions(info), + }, + { + title: 'Job Type', + description: jobType, + }, + { + title: 'Content Type', + description: get(info, 'output.content_type') || NA, + }, + { + title: 'Size in Bytes', + description: get(info, 'output.size') || NA, + }, + ]; + const jobInfoStatus: JobInfo[] = [ + { + title: 'Attempts', + description: attempts, + }, + { + title: 'Max Attempts', + description: maxAttempts, + }, + { + title: 'Priority', + description: priority, + }, + { + title: 'Timeout', + description: timeout, + }, + { + title: 'Status', + description: info.status || NA, + }, + { + title: 'Browser Type', + description: USES_HEADLESS_JOB_TYPES.includes(jobType) ? info.browser_type || UNKNOWN : NA, + }, + ]; if (warnings) { - jobInfoParts.status.push({ + jobInfoStatus.push({ title: 'Errors', description: warnings, }); } + const jobInfoParts: JobInfoMap = { + datetimes: jobInfoDateTimes, + payload: jobInfoPayload, + status: jobInfoStatus, + }; + return ( { return (
{ }} /> {maxSizeReached} + {warnings}
); } From ce27fa17666ef35357634f26e1f23ba8ebef255c Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 13 Mar 2020 14:02:44 -0700 Subject: [PATCH 20/23] Move relative pathing to absolute --- x-pack/plugins/reporting/index.d.ts | 2 -- .../plugins/reporting/public/components/general_error.tsx | 2 +- .../plugins/reporting/public/components/job_failure.tsx | 2 +- .../plugins/reporting/public/components/job_success.tsx | 2 +- .../reporting/public/components/job_warning_formulas.tsx | 2 +- .../reporting/public/components/job_warning_max_size.tsx | 2 +- .../reporting/public/components/report_listing.tsx | 2 +- .../public/components/reporting_panel_content.tsx | 2 +- .../public/components/screen_capture_panel_content.tsx | 2 +- x-pack/plugins/reporting/public/index.ts | 2 +- .../plugins/reporting/public/lib/reporting_api_client.ts | 2 +- .../plugins/reporting/public/lib/stream_handler.test.ts | 2 +- .../public/panel_actions/get_csv_panel_action.tsx | 5 ++--- x-pack/plugins/reporting/public/plugin.tsx | 8 +------- .../public/share_context_menu/register_csv_reporting.tsx | 2 +- .../share_context_menu/register_pdf_png_reporting.tsx | 2 +- 16 files changed, 16 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index dcdb3b6c0839f..7c1a2ebd7d9de 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -13,8 +13,6 @@ import { NotificationsStart, } from '../../../src/core/public'; -export { ToastsSetup, HttpSetup, ApplicationStart, IUiSettingsClient } from 'src/core/public'; - export type JobId = string; export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; diff --git a/x-pack/plugins/reporting/public/components/general_error.tsx b/x-pack/plugins/reporting/public/components/general_error.tsx index feb0ea0062ace..bc1ec901cc475 100644 --- a/x-pack/plugins/reporting/public/components/general_error.tsx +++ b/x-pack/plugins/reporting/public/components/general_error.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput => ({ diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 7544cbf906458..628ecb56b9c21 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobSummary, ManagementLinkFn } from '../../index.d'; diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index b538cef030e0d..c2feac382ca7a 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index 7981237c9b781..22f656dbe738c 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index caeda6fc01678..1abba8888bb81 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index f82b15470a1be..13fca019f3284 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -22,7 +22,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { ToastsSetup, ApplicationStart } from '../../'; +import { ToastsSetup, ApplicationStart } from 'src/core/public'; import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; import { Poller } from '../../common/poller'; import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 9ca98664d081d..6bff50de63335 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -8,9 +8,9 @@ import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@el import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; import url from 'url'; +import { ToastsSetup } from 'src/core/public'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { ToastsSetup } from '../../'; interface Props { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index b1ebb2583e5eb..9fb74a70ff1ac 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -7,9 +7,9 @@ import { EuiSpacer, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; +import { ToastsSetup } from 'src/core/public'; import { ReportingPanelContent } from './reporting_panel_content'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { ToastsSetup } from '../../'; interface Props { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 7e70f68096fbe..185367a85bdc0 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/public'; +import { PluginInitializerContext } from 'src/core/public'; import { ReportingPublicPlugin } from './plugin'; import * as jobCompletionNotifications from './lib/job_completion_notifications'; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index abc6f7559125f..ddfeb144d3cd7 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -7,8 +7,8 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; +import { HttpSetup } from 'src/core/public'; import { add } from './job_completion_notifications'; -import { HttpSetup } from '../../'; import { API_LIST_URL, API_BASE_URL, diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 9c92825fd8f47..3a2c7de9ad0f0 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -5,7 +5,7 @@ */ import sinon, { stub } from 'sinon'; -import { NotificationsStart } from '../../../../../src/core/public'; +import { NotificationsStart } from 'src/core/public'; import { SourceJob, JobSummary } from '../../index.d'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 7a1fbd49f9f7e..282ee75815fa5 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,12 +6,11 @@ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; +import { CoreSetup } from 'src/core/public'; +import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { checkLicense } from '../lib/license_check'; -import { CoreSetup } from '../../../../../src/core/public'; -import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; - import { ViewMode, IEmbeddable, diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 6454c3de9e655..08ba10ff69207 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -10,6 +10,7 @@ import ReactDOM from 'react-dom'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { ManagementSetup } from 'src/plugins/management/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { I18nProvider } from '@kbn/i18n/react'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; @@ -31,13 +32,6 @@ import { HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, -} from '../../../../src/core/public'; - import { JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG, JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 8f544c091b79c..9d4f475cde79a 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import { ToastsSetup } from 'src/core/public'; import { ReportingPanelContent } from '../components/reporting_panel_content'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { checkLicense } from '../lib/license_check'; import { LicensingPluginSetup } from '../../../licensing/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; -import { ToastsSetup } from '../..'; interface ReportingProvider { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 0bccea385bd4c..e9eaa9c2ed2a1 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; +import { ToastsSetup, IUiSettingsClient } from 'src/core/public'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { checkLicense } from '../lib/license_check'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; import { LicensingPluginSetup } from '../../../licensing/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; -import { ToastsSetup, IUiSettingsClient } from '../../'; interface ReportingPDFPNGProvider { apiClient: ReportingAPIClient; From 210d6278a88cb20336336863288b2c46088be075 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 16 Mar 2020 10:17:44 -0700 Subject: [PATCH 21/23] Fix POST URL copying as we've moved from static methods --- .../components/reporting_panel_content.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 6bff50de63335..150222bbd35bd 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -40,7 +40,7 @@ class ReportingPanelContentUi extends Component { this.state = { isStale: false, - absoluteUrl: '', + absoluteUrl: this.getAbsoluteReportGenerationUrl(props), layoutId: '', }; } @@ -53,15 +53,15 @@ class ReportingPanelContentUi extends Component { return url.resolve(window.location.href, relativePath); }; - public getDerivedStateFromProps = (nextProps: Props, prevState: State) => { - if (nextProps.layoutId !== prevState.layoutId) { - return { + public componentDidUpdate(prevProps: Props, prevState: State) { + if (this.props.layoutId !== prevState.layoutId) { + this.setState({ ...prevState, - absoluteUrl: this.getAbsoluteReportGenerationUrl(nextProps), - }; + absoluteUrl: this.getAbsoluteReportGenerationUrl(this.props), + layoutId: this.props.layoutId, + }); } - return prevState; - }; + } public componentWillUnmount() { window.removeEventListener('hashchange', this.markAsStale); From b002fd5e35f7ebeca63cb9ed16361f3dc8a625be Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 16 Mar 2020 11:22:34 -0700 Subject: [PATCH 22/23] Fix layoutId props --- .../reporting/public/components/reporting_panel_content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 150222bbd35bd..93a354f8d1296 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -16,7 +16,7 @@ interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; - layoutId: string | undefined; + layoutId: string; objectId?: string; objectType: string; getJobParams: () => any; From 9c2ecb7a4d3de3ee43a82bd78101469875563d85 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 16 Mar 2020 12:03:51 -0700 Subject: [PATCH 23/23] Fixes types for layoutId --- .../reporting/public/components/reporting_panel_content.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 93a354f8d1296..cf107fd712876 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -16,7 +16,7 @@ interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; - layoutId: string; + layoutId: string | undefined; objectId?: string; objectType: string; getJobParams: () => any; @@ -54,7 +54,7 @@ class ReportingPanelContentUi extends Component { }; public componentDidUpdate(prevProps: Props, prevState: State) { - if (this.props.layoutId !== prevState.layoutId) { + if (this.props.layoutId && this.props.layoutId !== prevState.layoutId) { this.setState({ ...prevState, absoluteUrl: this.getAbsoluteReportGenerationUrl(this.props),