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