diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 3562834230ea1..c01e6377b039e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; +import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; @@ -152,5 +154,21 @@ export function jobsQueryFactory(server: ServerFacade, elasticsearch: Elasticsea return hits[0]; }); }, + + async delete(deleteIndex: string, id: string) { + try { + const query = { id, index: deleteIndex }; + return callAsInternalUser('delete', query); + } catch (error) { + const wrappedError = new Error( + i18n.translate('xpack.reporting.jobsQuery.deleteError', { + defaultMessage: 'Could not delete the report: {error}', + values: { error: error.message }, + }) + ); + + throw Boom.boomify(wrappedError, { statusCode: error.status }); + } + }, }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index 2de420e6577c3..b9aa75e0ddd00 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -18,9 +18,13 @@ import { } from '../../types'; import { jobsQueryFactory } from '../lib/jobs_query'; import { ReportingSetupDeps, ReportingCore } from '../types'; -import { jobResponseHandlerFactory } from './lib/job_response_handler'; +import { + deleteJobResponseHandlerFactory, + downloadJobResponseHandlerFactory, +} from './lib/job_response_handler'; import { makeRequestFacade } from './lib/make_request_facade'; import { + getRouteConfigFactoryDeletePre, getRouteConfigFactoryDownloadPre, getRouteConfigFactoryManagementPre, } from './lib/route_config_factories'; @@ -40,7 +44,6 @@ export function registerJobInfoRoutes( const { elasticsearch } = plugins; const jobsQuery = jobsQueryFactory(server, elasticsearch); const getRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); - const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger); // list jobs in the queue, paginated server.route({ @@ -138,7 +141,8 @@ export function registerJobInfoRoutes( // trigger a download of the output from a job const exportTypesRegistry = reporting.getExportTypesRegistry(); - const jobResponseHandler = jobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); + const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger); + const downloadResponseHandler = downloadJobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); // prettier-ignore server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', @@ -147,7 +151,47 @@ export function registerJobInfoRoutes( const request = makeRequestFacade(legacyRequest); const { docId } = request.params; - let response = await jobResponseHandler( + let response = await downloadResponseHandler( + request.pre.management.jobTypes, + request.pre.user, + h, + { docId } + ); + + if (isResponse(response)) { + const { statusCode } = response; + + if (statusCode !== 200) { + if (statusCode === 500) { + logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); + } else { + logger.debug( + `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( + response.source + )}]` + ); + } + } + + response = response.header('accept-ranges', 'none'); + } + + return response; + }, + }); + + // allow a report to be deleted + const getRouteConfigDelete = getRouteConfigFactoryDeletePre(server, plugins, logger); + const deleteResponseHandler = deleteJobResponseHandlerFactory(server, elasticsearch); + server.route({ + path: `${MAIN_ENTRY}/delete/{docId}`, + method: 'DELETE', + options: getRouteConfigDelete(), + handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { + const request = makeRequestFacade(legacyRequest); + const { docId } = request.params; + + let response = await deleteResponseHandler( request.pre.management.jobTypes, request.pre.user, h, diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 62f0d0a72b389..30627d5b23230 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -20,7 +20,7 @@ interface JobResponseHandlerOpts { excludeContent?: boolean; } -export function jobResponseHandlerFactory( +export function downloadJobResponseHandlerFactory( server: ServerFacade, elasticsearch: ElasticsearchServiceSetup, exportTypesRegistry: ExportTypesRegistry @@ -36,6 +36,7 @@ export function jobResponseHandlerFactory( opts: JobResponseHandlerOpts = {} ) { const { docId } = params; + // TODO: async/await return jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }).then(doc => { if (!doc) return Boom.notFound(); @@ -67,3 +68,34 @@ export function jobResponseHandlerFactory( }); }; } + +export function deleteJobResponseHandlerFactory( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup +) { + const jobsQuery = jobsQueryFactory(server, elasticsearch); + + return async function deleteJobResponseHander( + validJobTypes: string[], + user: any, + h: ResponseToolkit, + params: JobResponseHandlerParams + ) { + const { docId } = params; + const doc = await jobsQuery.get(user, docId, { includeContent: false }); + if (!doc) return Boom.notFound(); + + const { jobtype: jobType } = doc._source; + if (!validJobTypes.includes(jobType)) { + return Boom.unauthorized(`Sorry, you are not authorized to delete ${jobType} reports`); + } + + try { + const docIndex = doc._index; + await jobsQuery.delete(docIndex, docId); + return h.response({ deleted: true }); + } catch (error) { + return Boom.boomify(error, { statusCode: error.statusCode }); + } + }; +} diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts index 82ba9ba22c706..3d275d34e2f7d 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts @@ -106,7 +106,22 @@ export function getRouteConfigFactoryDownloadPre( const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); return (): RouteConfigFactory => ({ ...getManagementRouteConfig(), - tags: [API_TAG], + tags: [API_TAG, 'download'], + response: { + ranges: false, + }, + }); +} + +export function getRouteConfigFactoryDeletePre( + server: ServerFacade, + plugins: ReportingSetupDeps, + logger: Logger +): GetRouteConfigFactoryFn { + const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); + return (): RouteConfigFactory => ({ + ...getManagementRouteConfig(), + tags: [API_TAG, 'delete'], response: { ranges: false, }, diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 6e79dca6eea9e..fa37832f9b636 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -197,6 +197,7 @@ export interface JobDocPayload { export interface JobSource { _id: string; + _index: string; _source: { jobtype: string; output: JobDocOutput; 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 index b5304c6020c43..1da95dd0ba197 100644 --- 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 @@ -2,6 +2,526 @@ exports[`ReportListing Report job listing with some items 1`] = ` Array [ + +
+ + +
+ +
+ + + +
+
+ + + + +
+ + +
+
+ + + +
+ +
+ + + +
+ + +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+
+ + Report + +
+
+
+ + Created at + +
+
+
+ + Status + +
+
+
+ + Actions + +
+
+
+ + Loading reports + +
+
+
+
+
+ +
+ ,
+ > + + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+
+ + +
+ +
+ > + + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+
+ + +
+ + Promise; +type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; +interface State { + showConfirm: boolean; +} + +export class ReportDeleteButton extends PureComponent { + constructor(props: Props) { + super(props); + this.state = { showConfirm: false }; + } + + private hideConfirm() { + this.setState({ showConfirm: false }); + } + + private showConfirm() { + this.setState({ showConfirm: true }); + } + + private renderConfirm() { + const { intl, jobsToDelete } = this.props; + + const title = + jobsToDelete.length > 1 + ? intl.formatMessage( + { + id: 'xpack.reporting.listing.table.deleteNumConfirmTitle', + defaultMessage: `Delete {num} reports?`, + }, + { num: jobsToDelete.length } + ) + : intl.formatMessage( + { + id: 'xpack.reporting.listing.table.deleteConfirmTitle', + defaultMessage: `Delete the "{name}" report?`, + }, + { name: jobsToDelete[0].object_title } + ); + const message = intl.formatMessage({ + id: 'xpack.reporting.listing.table.deleteConfirmMessage', + defaultMessage: `You can't recover deleted reports.`, + }); + const confirmButtonText = intl.formatMessage({ + id: 'xpack.reporting.listing.table.deleteConfirmButton', + defaultMessage: `Delete`, + }); + const cancelButtonText = intl.formatMessage({ + id: 'xpack.reporting.listing.table.deleteCancelButton', + defaultMessage: `Cancel`, + }); + + return ( + + this.hideConfirm()} + onConfirm={() => this.props.performDelete()} + confirmButtonText={confirmButtonText} + cancelButtonText={cancelButtonText} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + + + ); + } + + public render() { + const { jobsToDelete, intl } = this.props; + if (jobsToDelete.length === 0) return null; + + return ( + + this.showConfirm()} iconType="trash" color={'danger'}> + {intl.formatMessage( + { + id: 'xpack.reporting.listing.table.deleteReportButton', + defaultMessage: `Delete ({num})`, + }, + { num: jobsToDelete.length } + )} + + {this.state.showConfirm ? this.renderConfirm() : null} + + ); + } +} diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx new file mode 100644 index 0000000000000..b0674c149609d --- /dev/null +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -0,0 +1,72 @@ +/* + * 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, EuiToolTip } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { JobStatuses } from '../../../constants'; +import { Job as ListingJob, Props as ListingProps } from '../report_listing'; + +type Props = { record: ListingJob } & ListingProps; + +export const ReportDownloadButton: FunctionComponent = (props: Props) => { + const { record, apiClient, intl } = props; + + if (record.status !== JobStatuses.COMPLETED) { + return null; + } + + const button = ( + apiClient.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} + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx similarity index 83% rename from x-pack/plugins/reporting/public/components/report_error_button.tsx rename to x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 252dee9c619a9..1e33cc0188b8c 100644 --- a/x-pack/plugins/reporting/public/components/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -7,12 +7,14 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client'; +import { JobStatuses } from '../../../constants'; +import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; +import { Job as ListingJob } from '../report_listing'; interface Props { - jobId: string; intl: InjectedIntl; apiClient: ReportingAPIClient; + record: ListingJob; } interface State { @@ -39,12 +41,18 @@ class ReportErrorButtonUi extends Component { } public render() { + const { record, intl } = this.props; + + if (record.status !== JobStatuses.FAILED) { + return null; + } + const button = ( { }; private loadError = async () => { + const { record, apiClient, intl } = this.props; + this.setState({ isLoading: true }); try { - const reportContent: JobContent = await this.props.apiClient.getContent(this.props.jobId); + const reportContent: JobContent = await apiClient.getContent(record.id); if (this.mounted) { this.setState({ isLoading: false, error: reportContent.content }); } @@ -99,7 +109,7 @@ class ReportErrorButtonUi extends Component { if (this.mounted) { this.setState({ isLoading: false, - calloutTitle: this.props.intl.formatMessage({ + calloutTitle: intl.formatMessage({ id: 'xpack.reporting.errorButton.unableToFetchReportContentTitle', defaultMessage: 'Unable to fetch report content', }), diff --git a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.test.tsx similarity index 94% rename from x-pack/plugins/reporting/public/components/report_info_button.test.tsx rename to x-pack/plugins/reporting/public/components/buttons/report_info_button.test.tsx index 2edd59e6de7a3..028a8e960040a 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.test.tsx @@ -7,9 +7,10 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportInfoButton } from './report_info_button'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; -jest.mock('../lib/reporting_api_client'); +jest.mock('../../lib/reporting_api_client'); + +import { ReportingAPIClient } from '../../lib/reporting_api_client'; const httpSetup = {} as any; const apiClient = new ReportingAPIClient(httpSetup); diff --git a/x-pack/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx similarity index 98% rename from x-pack/plugins/reporting/public/components/report_info_button.tsx rename to x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx index 81a5af3b87957..941baa5af6776 100644 --- a/x-pack/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import React, { Component, Fragment } from 'react'; import { get } from 'lodash'; -import { USES_HEADLESS_JOB_TYPES } from '../../constants'; -import { JobInfo, ReportingAPIClient } from '../lib/reporting_api_client'; +import { USES_HEADLESS_JOB_TYPES } from '../../../constants'; +import { JobInfo, ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { jobId: string; diff --git a/x-pack/plugins/reporting/public/components/download_button.tsx b/x-pack/plugins/reporting/public/components/job_download_button.tsx similarity index 100% rename from x-pack/plugins/reporting/public/components/download_button.tsx rename to x-pack/plugins/reporting/public/components/job_download_button.tsx diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index c2feac382ca7a..ad16a506aeb70 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; +import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; -import { DownloadButton } from './download_button'; export const getSuccessToast = ( job: JobSummary, 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 22f656dbe738c..8717ae16d1ba1 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; +import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; -import { DownloadButton } from './download_button'; export const getWarningFormulasToast = ( job: JobSummary, 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 1abba8888bb81..83fa129f0715a 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 @@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; +import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; -import { DownloadButton } from './download_button'; export const getWarningMaxSizeToast = ( job: JobSummary, 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 5cf894580eae0..9b541261a690b 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -5,12 +5,15 @@ */ import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportListing } from './report_listing'; import { Observable } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ILicense } from '../../../licensing/public'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +jest.mock('@elastic/eui/lib/components/form/form_row/make_id', () => () => 'generated-id'); + +import { ReportListing } from './report_listing'; + const reportingAPIClient = { list: () => Promise.resolve([ diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 85ccced8d75f0..008cc154b1b39 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -4,34 +4,34 @@ * 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 { get } from 'lodash'; -import moment from 'moment'; -import React, { Component } from 'react'; -import { Subscription } from 'rxjs'; - import { - EuiBasicTable, - EuiButtonIcon, + EuiInMemoryTable, EuiPageContent, EuiSpacer, EuiText, EuiTextColor, EuiTitle, - EuiToolTip, } from '@elastic/eui'; - -import { ToastsSetup, ApplicationStart } from 'src/core/public'; -import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { get } from 'lodash'; +import moment from 'moment'; +import { Component, default as React } from 'react'; +import { Subscription } from 'rxjs'; +import { ApplicationStart, ToastsSetup } from 'src/core/public'; +import { ILicense, LicensingPluginSetup } 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'; import { checkLicense } from '../lib/license_check'; -import { ReportErrorButton } from './report_error_button'; -import { ReportInfoButton } from './report_info_button'; +import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; +import { + ReportDeleteButton, + ReportDownloadButton, + ReportErrorButton, + ReportInfoButton, +} from './buttons'; -interface Job { +export interface Job { id: string; type: string; object_type: string; @@ -49,7 +49,7 @@ interface Job { warnings: string[]; } -interface Props { +export interface Props { intl: InjectedIntl; apiClient: ReportingAPIClient; license$: LicensingPluginSetup['license$']; @@ -61,6 +61,7 @@ interface State { page: number; total: number; jobs: Job[]; + selectedJobs: Job[]; isLoading: boolean; showLinks: boolean; enableLinks: boolean; @@ -113,6 +114,7 @@ class ReportListingUi extends Component { page: 0, total: 0, jobs: [], + selectedJobs: [], isLoading: false, showLinks: false, enableLinks: false, @@ -182,6 +184,140 @@ class ReportListingUi extends Component { }); }; + private onSelectionChange = (jobs: Job[]) => { + this.setState(current => ({ ...current, selectedJobs: jobs })); + }; + + private removeRecord = (record: Job) => { + const { jobs } = this.state; + const filtered = jobs.filter(j => j.id !== record.id); + this.setState(current => ({ ...current, jobs: filtered })); + }; + + private renderDeleteButton = () => { + const { selectedJobs } = this.state; + if (selectedJobs.length === 0) return null; + + const performDelete = async () => { + for (const record of selectedJobs) { + try { + await this.props.apiClient.deleteReport(record.id); + this.removeRecord(record); + this.props.toasts.addSuccess( + this.props.intl.formatMessage( + { + id: 'xpack.reporting.listing.table.deleteConfim', + defaultMessage: `The {reportTitle} report was deleted`, + }, + { reportTitle: record.object_title } + ) + ); + } catch (error) { + this.props.toasts.addDanger( + this.props.intl.formatMessage( + { + id: 'xpack.reporting.listing.table.deleteFailedErrorMessage', + defaultMessage: `The report was not deleted: {error}`, + }, + { error } + ) + ); + throw error; + } + } + }; + + 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.apiClient.list(this.state.page); + total = await this.props.apiClient.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.type, + 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.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; + } + } + private renderTable() { const { intl } = this.props; @@ -317,9 +453,9 @@ class ReportListingUi extends Component { render: (record: Job) => { return (
- {this.renderDownloadButton(record)} - {this.renderReportErrorButton(record)} - {this.renderInfoButton(record)} + + +
); }, @@ -335,13 +471,22 @@ class ReportListingUi extends Component { hidePerPageOptions: true, }; + const selection = { + itemId: 'id', + onSelectionChange: this.onSelectionChange, + }; + + const search = { + toolsRight: this.renderDeleteButton(), + }; + return ( - { }) } pagination={pagination} + selection={selection} + search={search} + isSelectable={true} onChange={this.onTableChange} data-test-subj="reportJobListing" /> ); } - - private renderDownloadButton = (record: Job) => { - if (record.status !== JobStatuses.COMPLETED) { - return; - } - - const { intl } = this.props; - const button = ( - this.props.apiClient.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.apiClient.list(this.state.page); - total = await this.props.apiClient.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.type, - 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.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/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index ddfeb144d3cd7..cddfcd3ec855a 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -85,6 +85,12 @@ export class ReportingAPIClient { window.open(location); } + public async deleteReport(jobId: string) { + return await this.http.delete(`${API_LIST_URL}/delete/${jobId}`, { + asSystemRequest: true, + }); + } + public list = (page = 0, jobIds: string[] = []): Promise => { const query = { page } as any; if (jobIds.length > 0) {