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..f211116c12a19 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap @@ -0,0 +1,565 @@ +// 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 [ + + + + + +
+
+ + + + +
+ +

+ 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 [ + + + + + +
+
+ + + + +
+ +

+ Job Info +

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

+ Job Info +

+
+
+
+ +
+ +
+ +
+ +
, +] +`; 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..93ceed0f64a0e --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx @@ -0,0 +1,57 @@ +/* + * 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. + */ + +const mockJobQueueClient = { getInfo: jest.fn() }; +jest.mock('../lib/job_queue_client', () => ({ jobQueueClient: mockJobQueueClient })); + +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..4f8f330c6f5ee --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -0,0 +1,259 @@ +/* + * 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 { get } from 'lodash'; +import React, { Component, Fragment } from 'react'; +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 getDimensions = (info: JobInfo) => { + 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; + } + + // TODO browser type + // TODO queue method (clicked UI, watcher, etc) + const jobInfoParts = { + datetimes: [ + { + title: 'Created By', + description: get(info, 'created_by', NA), + }, + { + title: 'Created At', + description: get(info, 'created_at', NA), + }, + { + title: 'Started At', + description: get(info, 'started_at', NA), + }, + { + title: 'Completed At', + description: get(info, 'completed_at', NA), + }, + { + 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: get(info, 'jobtype', NA), + }, + { + title: 'Content Type', + description: get(info, 'output.content_type') || NA, + }, + ], + status: [ + { + title: 'Attempts', + description: get(info, 'attempts', NA), + }, + { + title: 'Max Attempts', + description: get(info, 'max_attempts', NA), + }, + { + title: 'Priority', + description: get(info, 'priority', NA), + }, + { + title: 'Timeout', + description: get(info, 'timeout', NA), + }, + { + title: 'Status', + description: get(info, 'status', 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 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, + }); + throw 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.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index e109df0e03ba1..057e1dedf3f16 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -17,6 +17,7 @@ import { Poller } from '../../../../common/poller'; 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'; import { EuiBasicTable, @@ -189,6 +190,7 @@ export class ReportListing extends Component {
{this.renderDownloadButton(record)} {this.renderReportErrorButton(record)} + {this.renderInfoButton(record)}
); }, @@ -249,6 +251,10 @@ export class ReportListing extends Component { return ; }; + private renderInfoButton = (record: Job) => { + return ; + }; + private onTableChange = ({ page }: { page: { index: number } }) => { const { index: pageIndex } = page; 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 75080dd4474fd..5c979eebf21af 100644 --- a/x-pack/plugins/reporting/public/lib/job_queue_client.ts +++ b/x-pack/plugins/reporting/public/lib/job_queue_client.ts @@ -20,6 +20,33 @@ export interface JobContent { content_type: boolean; } +export interface JobInfo { + created_at: string; + priority: number; + jobtype: string; + created_by: string; + timeout: number; + output: { content_type: 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; @@ -50,6 +77,14 @@ class JobQueueClient { headers: addSystemApiHeader({}), }); } + + public getInfo(jobId: string): Promise { + return kfetch({ + method: 'GET', + pathname: `${API_BASE_URL}/info/${jobId}`, + headers: addSystemApiHeader({}), + }); + } } export const jobQueueClient = new JobQueueClient(); diff --git a/x-pack/plugins/reporting/server/routes/jobs.js b/x-pack/plugins/reporting/server/routes/jobs.js index f656ad1c7eb73..2639fc103bbde 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.js +++ b/x-pack/plugins/reporting/server/routes/jobs.js @@ -81,6 +81,36 @@ export function jobs(server) { config: getRouteConfig(), }); + // return some info about the job + server.route({ + path: `${mainEntry}/info/{docId}`, + method: 'GET', + handler: (request) => { + const { docId } = request.params; + + return jobsQuery.get(request.pre.user, docId) + .then((doc) => { + if (!doc) { + return boom.notFound(); + } + + const { jobtype: jobType } = doc._source; + if (!request.pre.management.jobTypes.includes(jobType)) { + return boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); + } + + const { payload } = doc._source; + payload.headers = 'not shown'; + + return { + ...doc._source, + payload + }; + }); + }, + config: getRouteConfig(), + }); + // trigger a download of the output from a job // NOTE: We're disabling range request for downloading the PDF. There's a bug in Firefox's PDF.js viewer // (https://github.com/mozilla/pdf.js/issues/8958) where they're using a range request to retrieve the