From 476bcb474c76f1522974e9e003a5614c55c00326 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 10 Mar 2020 10:20:58 -0700 Subject: [PATCH 01/10] [Reporting] Feature Delete Button in Job Listing --- .../reporting/server/lib/jobs_query.ts | 18 +++ .../plugins/reporting/server/routes/jobs.ts | 52 ++++++- .../server/routes/lib/get_document_payload.ts | 3 +- .../server/routes/lib/job_response_handler.ts | 34 ++++- .../routes/lib/route_config_factories.ts | 17 ++- x-pack/legacy/plugins/reporting/types.d.ts | 1 + .../reporting/public/components/buttons.tsx | 132 ++++++++++++++++++ .../public/components/report_listing.tsx | 132 +++++++++--------- .../public/lib/reporting_api_client.ts | 6 + 9 files changed, 319 insertions(+), 76 deletions(-) create mode 100644 x-pack/plugins/reporting/public/components/buttons.tsx 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 3562834230ea1d..c01e6377b039e5 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 2de420e6577c3c..b9aa75e0ddd000 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/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index fb3944ea33552f..a196b70038e53e 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -65,10 +65,11 @@ export function getDocumentPayloadFactory( const exportType = exportTypesRegistry.get((item: ExportTypeType) => item.jobType === jobType); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); + const content = encodeContent(output.content, exportType); return { statusCode: 200, - content: encodeContent(output.content, exportType), + content, contentType: output.content_type, headers: { ...headers, 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 62f0d0a72b389a..30627d5b232301 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 82ba9ba22c7061..3d275d34e2f7d6 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 917e9d7daae407..238079ba92a291 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/buttons.tsx b/x-pack/plugins/reporting/public/components/buttons.tsx new file mode 100644 index 00000000000000..f48e8e51b147fb --- /dev/null +++ b/x-pack/plugins/reporting/public/components/buttons.tsx @@ -0,0 +1,132 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { ReportErrorButton } from './report_error_button'; +import { ReportInfoButton } from './report_info_button'; +import { JobStatuses } from '../../constants'; +import { Job as ListingJob, Props as ListingProps } from './report_listing'; + +const { COMPLETED, FAILED } = JobStatuses; + +export const renderDownloadButton = (props: ListingProps, record: ListingJob) => { + if (record.status !== COMPLETED) { + return; + } + + const { intl } = props; + const button = ( + 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} + + ); +}; + +// callback for updating the listing after delete response arrives +type DeleteHandler = () => Promise; + +export const renderDeleteButton = ( + props: ListingProps, + handleDelete: DeleteHandler, + record: ListingJob +) => { + if (!([COMPLETED, FAILED] as string[]).includes(record.status)) { + return; + } + + const { intl } = props; + const button = ( + + ); + return ( + + {button} + + ); +}; + +export const renderReportErrorButton = (props: ListingProps, record: ListingJob) => { + if (record.status !== FAILED) { + return; + } + + return ; +}; + +export const renderInfoButton = (props: ListingProps, record: ListingJob) => { + const { intl } = props; + return ( + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 13fca019f32840..b8e2c844481c0f 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -4,34 +4,35 @@ * 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, + EuiLoadingSpinner, 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 React, { Component, default as React, 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 { + renderDeleteButton, + renderDownloadButton, + renderInfoButton, + renderReportErrorButton, +} from './buttons'; -interface Job { +export interface Job { id: string; type: string; object_type: string; @@ -47,9 +48,10 @@ interface Job { max_attempts: number; csv_contains_formulas: boolean; warnings: string[]; + is_deleting: boolean; } -interface Props { +export interface Props { intl: InjectedIntl; apiClient: ReportingAPIClient; license$: LicensingPluginSetup['license$']; @@ -182,6 +184,44 @@ class ReportListingUi extends Component { }); }; + 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 = (record: Job) => { + const handleDelete = async () => { + try { + // TODO present a modal to verify: this can not be undone + this.setState(current => ({ ...current, is_deleting: true })); + 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 renderDeleteButton(this.props, handleDelete, record); // FIXME react component + }; + private renderTable() { const { intl } = this.props; @@ -315,11 +355,13 @@ class ReportListingUi extends Component { actions: [ { render: (record: Job) => { + const canDelete = !record.is_deleting; return (
{this.renderDownloadButton(record)} {this.renderReportErrorButton(record)} {this.renderInfoButton(record)} + {canDelete ? this.renderDeleteButton(record) : }
); }, @@ -360,64 +402,15 @@ class ReportListingUi extends Component { } 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; + return renderDownloadButton(this.props, record); // FIXME react component }; private renderReportErrorButton = (record: Job) => { - if (record.status !== JobStatuses.FAILED) { - return; - } - - return ; + return renderReportErrorButton(this.props, record); // FIXME react component }; private renderInfoButton = (record: Job) => { - return ; + return renderInfoButton(this.props, record); // FIXME react component }; private onTableChange = ({ page }: { page: { index: number } }) => { @@ -482,6 +475,7 @@ class ReportListingUi extends Component { max_attempts: source.max_attempts, csv_contains_formulas: get(source, 'output.csv_contains_formulas'), warnings: source.output ? source.output.warnings : undefined, + is_deleting: false, }; } ), 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 ddfeb144d3cd74..cddfcd3ec855a6 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) { From e05372132b4dbb50572b4d5c31f4a85babffa756 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 13 Mar 2020 15:37:29 -0700 Subject: [PATCH 02/10] refactor listing buttons --- .../reporting/public/components/buttons.tsx | 132 ------------------ .../public/components/buttons/index.tsx | 22 +++ .../buttons/report_delete_button.tsx | 47 +++++++ .../buttons/report_download_button.tsx | 72 ++++++++++ .../{ => buttons}/report_error_button.tsx | 2 +- .../{ => buttons}/report_info_button.test.tsx | 2 +- .../{ => buttons}/report_info_button.tsx | 4 +- ...oad_button.tsx => job_download_button.tsx} | 0 .../public/components/job_success.tsx | 2 +- .../components/job_warning_formulas.tsx | 2 +- .../components/job_warning_max_size.tsx | 2 +- .../public/components/report_listing.tsx | 88 +++++------- 12 files changed, 186 insertions(+), 189 deletions(-) delete mode 100644 x-pack/plugins/reporting/public/components/buttons.tsx create mode 100644 x-pack/plugins/reporting/public/components/buttons/index.tsx create mode 100644 x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx create mode 100644 x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx rename x-pack/plugins/reporting/public/components/{ => buttons}/report_error_button.tsx (97%) rename x-pack/plugins/reporting/public/components/{ => buttons}/report_info_button.test.tsx (96%) rename x-pack/plugins/reporting/public/components/{ => buttons}/report_info_button.tsx (98%) rename x-pack/plugins/reporting/public/components/{download_button.tsx => job_download_button.tsx} (100%) diff --git a/x-pack/plugins/reporting/public/components/buttons.tsx b/x-pack/plugins/reporting/public/components/buttons.tsx deleted file mode 100644 index f48e8e51b147fb..00000000000000 --- a/x-pack/plugins/reporting/public/components/buttons.tsx +++ /dev/null @@ -1,132 +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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ReportErrorButton } from './report_error_button'; -import { ReportInfoButton } from './report_info_button'; -import { JobStatuses } from '../../constants'; -import { Job as ListingJob, Props as ListingProps } from './report_listing'; - -const { COMPLETED, FAILED } = JobStatuses; - -export const renderDownloadButton = (props: ListingProps, record: ListingJob) => { - if (record.status !== COMPLETED) { - return; - } - - const { intl } = props; - const button = ( - 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} - - ); -}; - -// callback for updating the listing after delete response arrives -type DeleteHandler = () => Promise; - -export const renderDeleteButton = ( - props: ListingProps, - handleDelete: DeleteHandler, - record: ListingJob -) => { - if (!([COMPLETED, FAILED] as string[]).includes(record.status)) { - return; - } - - const { intl } = props; - const button = ( - - ); - return ( - - {button} - - ); -}; - -export const renderReportErrorButton = (props: ListingProps, record: ListingJob) => { - if (record.status !== FAILED) { - return; - } - - return ; -}; - -export const renderInfoButton = (props: ListingProps, record: ListingJob) => { - const { intl } = props; - return ( - - - - ); -}; diff --git a/x-pack/plugins/reporting/public/components/buttons/index.tsx b/x-pack/plugins/reporting/public/components/buttons/index.tsx new file mode 100644 index 00000000000000..65758e851b69b4 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/buttons/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 { JobStatuses } from '../../../constants'; +import { Job, Props as ListingProps } from '../report_listing'; +import { ReportErrorButton as ErrorButton } from './report_error_button'; + +export const ReportErrorButton = ({ record, ...props }: { record: Job } & ListingProps) => { + if (record.status !== JobStatuses.FAILED) { + return null; + } + + return ; +}; + +export { ReportDeleteButton } from './report_delete_button'; +export { ReportDownloadButton } from './report_download_button'; +export { ReportInfoButton } from './report_info_button'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx new file mode 100644 index 00000000000000..4eb24e527ccecb --- /dev/null +++ b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx @@ -0,0 +1,47 @@ +/* + * 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 from 'react'; +import { JobStatuses } from '../../../constants'; +import { Job as ListingJob, Props as ListingProps } from '../report_listing'; + +const { COMPLETED, FAILED } = JobStatuses; +type DeleteHandler = () => Promise; + +export const ReportDeleteButton = ({ + record, + handleDelete, + ...props +}: { record: ListingJob; handleDelete: DeleteHandler } & ListingProps) => { + if (!([COMPLETED, FAILED] as string[]).includes(record.status)) { + return null; + } + + const { intl } = props; + const button = ( + + ); + return ( + + {button} + + ); +}; 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 00000000000000..b0674c149609d1 --- /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 97% 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 252dee9c619a98..4b72180630c863 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,7 +7,7 @@ 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 { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { jobId: string; 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 96% 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 2edd59e6de7a38..2baba542bbdbb1 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,7 +7,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportInfoButton } from './report_info_button'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingAPIClient } from '../../lib/reporting_api_client'; jest.mock('../lib/reporting_api_client'); 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 81a5af3b87957d..941baa5af67765 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 c2feac382ca7ad..ad16a506aeb70f 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 22f656dbe738cf..8717ae16d1ba10 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 1abba8888bb818..83fa129f0715ab 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.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index b8e2c844481c0f..1c8ec905a90519 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import moment from 'moment'; -import React, { Component, default as React, default as React } from 'react'; +import React, { Component, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; @@ -26,10 +26,10 @@ import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../c import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; import { - renderDeleteButton, - renderDownloadButton, - renderInfoButton, - renderReportErrorButton, + ReportDeleteButton, + ReportDownloadButton, + ReportErrorButton, + ReportInfoButton, } from './buttons'; export interface Job { @@ -190,38 +190,6 @@ class ReportListingUi extends Component { this.setState(current => ({ ...current, jobs: filtered })); }; - private renderDeleteButton = (record: Job) => { - const handleDelete = async () => { - try { - // TODO present a modal to verify: this can not be undone - this.setState(current => ({ ...current, is_deleting: true })); - 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 renderDeleteButton(this.props, handleDelete, record); // FIXME react component - }; - private renderTable() { const { intl } = this.props; @@ -358,9 +326,9 @@ class ReportListingUi extends Component { const canDelete = !record.is_deleting; return (
- {this.renderDownloadButton(record)} - {this.renderReportErrorButton(record)} - {this.renderInfoButton(record)} + + + {canDelete ? this.renderDeleteButton(record) : }
); @@ -401,16 +369,36 @@ class ReportListingUi extends Component { ); } - private renderDownloadButton = (record: Job) => { - return renderDownloadButton(this.props, record); // FIXME react component - }; - - private renderReportErrorButton = (record: Job) => { - return renderReportErrorButton(this.props, record); // FIXME react component - }; - - private renderInfoButton = (record: Job) => { - return renderInfoButton(this.props, record); // FIXME react component + private renderDeleteButton = (record: Job) => { + const handleDelete = async () => { + try { + // TODO present a modal to verify: this can not be undone + this.setState(current => ({ ...current, is_deleting: true })); + 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 } }) => { From 10865f256a195b040ce063502dc3535cedf7fc26 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 13 Mar 2020 18:09:17 -0700 Subject: [PATCH 03/10] multi-delete --- .../buttons/report_delete_button.tsx | 61 ++++---- .../public/components/report_listing.tsx | 141 +++++++++++------- 2 files changed, 109 insertions(+), 93 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx index 4eb24e527ccecb..efdd0b05862247 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx @@ -4,44 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React from 'react'; -import { JobStatuses } from '../../../constants'; -import { Job as ListingJob, Props as ListingProps } from '../report_listing'; +import { EuiButton } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { Job, Props as ListingProps } from '../report_listing'; -const { COMPLETED, FAILED } = JobStatuses; -type DeleteHandler = () => Promise; +type DeleteFn = () => Promise; +type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; -export const ReportDeleteButton = ({ - record, - handleDelete, - ...props -}: { record: ListingJob; handleDelete: DeleteHandler } & ListingProps) => { - if (!([COMPLETED, FAILED] as string[]).includes(record.status)) { - return null; - } +export const ReportDeleteButton: FunctionComponent = (props: Props) => { + const { jobsToDelete, performDelete, intl } = props; + + if (jobsToDelete.length === 0) return null; + + const message = + jobsToDelete.length > 1 + ? intl.formatMessage( + { + id: 'xpack.reporting.listing.table.deleteReportButton', + defaultMessage: `Delete {num} reports`, + }, + { num: jobsToDelete.length } + ) + : intl.formatMessage({ + id: 'xpack.reporting.listing.table.deleteReportButton', + defaultMessage: `Delete report`, + }); - const { intl } = props; - const button = ( - - ); return ( - - {button} - + + {message} + ); }; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 1c8ec905a90519..1f373b2dffe44f 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -5,8 +5,7 @@ */ import { - EuiBasicTable, - EuiLoadingSpinner, + EuiInMemoryTable, EuiPageContent, EuiSpacer, EuiText, @@ -17,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import moment from 'moment'; -import React, { Component, Fragment } from 'react'; +import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; @@ -63,6 +62,7 @@ interface State { page: number; total: number; jobs: Job[]; + selectedJobs: Job[]; isLoading: boolean; showLinks: boolean; enableLinks: boolean; @@ -115,6 +115,7 @@ class ReportListingUi extends Component { page: 0, total: 0, jobs: [], + selectedJobs: [], isLoading: false, showLinks: false, enableLinks: false, @@ -184,10 +185,8 @@ class ReportListingUi extends Component { }); }; - private removeRecord = (record: Job) => { - const { jobs } = this.state; - const filtered = jobs.filter(j => j.id !== record.id); - this.setState(current => ({ ...current, jobs: filtered })); + private onSelectionChange = (jobs: Job[]) => { + this.setState(current => ({ ...current, selectedJobs: jobs })); }; private renderTable() { @@ -323,13 +322,11 @@ class ReportListingUi extends Component { actions: [ { render: (record: Job) => { - const canDelete = !record.is_deleting; return (
- + - {canDelete ? this.renderDeleteButton(record) : }
); }, @@ -345,60 +342,88 @@ class ReportListingUi extends Component { hidePerPageOptions: true, }; + const selection = { + itemId: 'id', + onSelectionChange: this.onSelectionChange, + }; + return ( - + + {this.renderDeleteButton()} + + + ); } - private renderDeleteButton = (record: Job) => { - const handleDelete = async () => { - try { - // TODO present a modal to verify: this can not be undone - this.setState(current => ({ ...current, is_deleting: true })); - 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; + 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 { + this.setState(current => ({ ...current, is_deleting: true })); + 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 ; + + return ( + + ); }; private onTableChange = ({ page }: { page: { index: number } }) => { From 06ecca906d42f044ee9dc4ffbc09dfd75737343c Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 13 Mar 2020 18:27:32 -0700 Subject: [PATCH 04/10] confirm modal --- .../buttons/report_delete_button.tsx | 115 ++++++++++++++---- 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx index efdd0b05862247..3f7c210eb3696b 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx @@ -4,35 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton } from '@elastic/eui'; -import React, { FunctionComponent } from 'react'; +import { EuiConfirmModal, EuiOverlayMask, EuiButton } from '@elastic/eui'; +import React, { PureComponent, Fragment } from 'react'; import { Job, Props as ListingProps } from '../report_listing'; type DeleteFn = () => Promise; type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; +interface State { + showConfirm: boolean; +} -export const ReportDeleteButton: FunctionComponent = (props: Props) => { - const { jobsToDelete, performDelete, intl } = props; - - if (jobsToDelete.length === 0) return null; - - const message = - jobsToDelete.length > 1 - ? intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteReportButton', - defaultMessage: `Delete {num} reports`, - }, - { num: jobsToDelete.length } - ) - : intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteReportButton', - defaultMessage: `Delete report`, - }); - - return ( - - {message} - - ); -}; +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} + + ); + } +} From a836b81febf07f480dcac087fc20aa15fcc15e96 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 16 Mar 2020 17:44:40 -0700 Subject: [PATCH 05/10] remove unused --- x-pack/plugins/reporting/public/components/report_listing.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 1f373b2dffe44f..47cc1a9ae9b4e2 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -47,7 +47,6 @@ export interface Job { max_attempts: number; csv_contains_formulas: boolean; warnings: string[]; - is_deleting: boolean; } export interface Props { @@ -390,7 +389,6 @@ class ReportListingUi extends Component { const performDelete = async () => { for (const record of selectedJobs) { try { - this.setState(current => ({ ...current, is_deleting: true })); await this.props.apiClient.deleteReport(record.id); this.removeRecord(record); this.props.toasts.addSuccess( @@ -488,7 +486,6 @@ class ReportListingUi extends Component { max_attempts: source.max_attempts, csv_contains_formulas: get(source, 'output.csv_contains_formulas'), warnings: source.output ? source.output.warnings : undefined, - is_deleting: false, }; } ), From c1e6e3e5631093129607135f53f9bfdcd92c72b8 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 16 Mar 2020 17:54:07 -0700 Subject: [PATCH 06/10] fix test --- .../server/routes/lib/get_document_payload.ts | 3 +- .../report_listing.test.tsx.snap | 613 +++++++++++++++++- .../report_info_button.test.tsx.snap | 0 .../buttons/report_info_button.test.tsx | 5 +- 4 files changed, 611 insertions(+), 10 deletions(-) rename x-pack/plugins/reporting/public/components/{ => buttons}/__snapshots__/report_info_button.test.tsx.snap (100%) diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index a196b70038e53e..fb3944ea33552f 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -65,11 +65,10 @@ export function getDocumentPayloadFactory( const exportType = exportTypesRegistry.get((item: ExportTypeType) => item.jobType === jobType); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); - const content = encodeContent(output.content, exportType); return { statusCode: 200, - content, + content: encodeContent(output.content, exportType), contentType: output.content_type, headers: { ...headers, 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 b5304c6020c43e..73cd258edd7d3c 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,411 @@ exports[`ReportListing Report job listing with some items 1`] = ` Array [ + + +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+
+ + Report + +
+
+
+ + Created at + +
+
+
+ + Status + +
+
+
+ + Actions + +
+
+
+ + Loading reports + +
+
+
+
+
+ + ,
+ > + + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+
+ + +
+ +
+ > + + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+
+ + +
+ + Date: Tue, 17 Mar 2020 14:21:18 -0700 Subject: [PATCH 07/10] mock the id generator for snapshotting --- .../report_listing.test.tsx.snap | 30 +++++++++---------- .../public/components/report_listing.test.tsx | 7 +++-- 2 files changed, 20 insertions(+), 17 deletions(-) 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 73cd258edd7d3c..afb1b7ed60bc92 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 @@ -143,7 +143,7 @@ Array [ checked={false} compressed={false} disabled={true} - id="_selection_column-checkbox_cafv69fr" + id="_selection_column-checkbox_generated-id" indeterminate={false} label="Select all rows" onChange={[Function]} @@ -156,7 +156,7 @@ Array [ checked={false} className="euiCheckbox__input" disabled={true} - id="_selection_column-checkbox_cafv69fr" + id="_selection_column-checkbox_generated-id" onChange={[Function]} type="checkbox" /> @@ -165,7 +165,7 @@ Array [ /> @@ -229,7 +229,7 @@ Array [ compressed={false} data-test-subj="checkboxSelectAll" disabled={true} - id="_selection_column-checkbox_fubvvpen" + id="_selection_column-checkbox_generated-id" indeterminate={false} label={null} onChange={[Function]} @@ -244,7 +244,7 @@ Array [ className="euiCheckbox__input" data-test-subj="checkboxSelectAll" disabled={true} - id="_selection_column-checkbox_fubvvpen" + id="_selection_column-checkbox_generated-id" onChange={[Function]} type="checkbox" /> @@ -496,7 +496,7 @@ Array [ checked={false} compressed={false} disabled={true} - id="_selection_column-checkbox_cafv69fr" + id="_selection_column-checkbox_generated-id" indeterminate={false} label="Select all rows" onChange={[Function]} @@ -509,7 +509,7 @@ Array [ checked={false} className="euiCheckbox__input" disabled={true} - id="_selection_column-checkbox_cafv69fr" + id="_selection_column-checkbox_generated-id" onChange={[Function]} type="checkbox" /> @@ -518,7 +518,7 @@ Array [ /> @@ -582,7 +582,7 @@ Array [ compressed={false} data-test-subj="checkboxSelectAll" disabled={true} - id="_selection_column-checkbox_fubvvpen" + id="_selection_column-checkbox_generated-id" indeterminate={false} label={null} onChange={[Function]} @@ -597,7 +597,7 @@ Array [ className="euiCheckbox__input" data-test-subj="checkboxSelectAll" disabled={true} - id="_selection_column-checkbox_fubvvpen" + id="_selection_column-checkbox_generated-id" onChange={[Function]} type="checkbox" /> @@ -791,7 +791,7 @@ Array [ checked={false} compressed={false} disabled={true} - id="_selection_column-checkbox_cafv69fr" + id="_selection_column-checkbox_generated-id" indeterminate={false} label="Select all rows" onChange={[Function]} @@ -804,7 +804,7 @@ Array [ checked={false} className="euiCheckbox__input" disabled={true} - id="_selection_column-checkbox_cafv69fr" + id="_selection_column-checkbox_generated-id" onChange={[Function]} type="checkbox" /> @@ -813,7 +813,7 @@ Array [ /> @@ -877,7 +877,7 @@ Array [ compressed={false} data-test-subj="checkboxSelectAll" disabled={true} - id="_selection_column-checkbox_fubvvpen" + id="_selection_column-checkbox_generated-id" indeterminate={false} label={null} onChange={[Function]} @@ -892,7 +892,7 @@ Array [ className="euiCheckbox__input" data-test-subj="checkboxSelectAll" disabled={true} - id="_selection_column-checkbox_fubvvpen" + id="_selection_column-checkbox_generated-id" onChange={[Function]} type="checkbox" /> 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 5cf894580eae03..9b541261a690ba 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([ From eb9b433008d6a0c0d31ca75482031315f636974a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 17 Mar 2020 14:44:09 -0700 Subject: [PATCH 08/10] simplify --- .../reporting/public/components/buttons/index.tsx | 11 +---------- .../public/components/buttons/report_error_button.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/buttons/index.tsx b/x-pack/plugins/reporting/public/components/buttons/index.tsx index 65758e851b69b4..3c8f34f1d1a8af 100644 --- a/x-pack/plugins/reporting/public/components/buttons/index.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/index.tsx @@ -7,16 +7,7 @@ import React from 'react'; import { JobStatuses } from '../../../constants'; import { Job, Props as ListingProps } from '../report_listing'; -import { ReportErrorButton as ErrorButton } from './report_error_button'; - -export const ReportErrorButton = ({ record, ...props }: { record: Job } & ListingProps) => { - if (record.status !== JobStatuses.FAILED) { - return null; - } - - return ; -}; - +export { ReportErrorButton } from './report_error_button'; export { ReportDeleteButton } from './report_delete_button'; export { ReportDownloadButton } from './report_download_button'; export { ReportInfoButton } from './report_info_button'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 4b72180630c863..897295d595f970 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -7,6 +7,7 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; +import { JobStatuses } from '../../../constants'; import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { @@ -39,12 +40,18 @@ class ReportErrorButtonUi extends Component { } public render() { + const { record, apiClient, intl } = this.props; + + if (record.status !== JobStatuses.FAILED) { + return null; + } + const button = ( Date: Tue, 17 Mar 2020 15:04:13 -0700 Subject: [PATCH 09/10] add search bar above table --- .../report_listing.test.tsx.snap | 717 ++++++++++-------- .../public/components/report_listing.tsx | 309 ++++---- 2 files changed, 571 insertions(+), 455 deletions(-) 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 afb1b7ed60bc92..1da95dd0ba1975 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 @@ -46,6 +46,11 @@ Array [ } } responsive={true} + search={ + Object { + "toolsRight": null, + } + } selection={ Object { "itemId": "id", @@ -54,332 +59,407 @@ Array [ } tableLayout="fixed" > - -
+ -
- -
+
+ - -
- -
- - -
+ +
- -
- - -
- - -
- -
- -
- - - - - - - - - - + - - - + + + + + + - + + + + + + + + + + + + +
+ +
-
- - +
+
-
+ + + + + + + + + + + + + + + + + -
- - Report - -
- -
- - + + -
- - Created at - -
- -
- - + + -
- - Status - -
- -
- - - - - - - - - - - + Status + + + + + - - + + - - - -
+ +
+
+ + +
+ +
+
+ + +
+
+ Report + + + + Created at + + + -
- - Actions - -
-
- Loading reports + Actions
-
-
+
+
+ + Loading reports + +
+
+
+
-
- + +
, { 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.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.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; @@ -346,164 +476,35 @@ class ReportListingUi extends Component { onSelectionChange: this.onSelectionChange, }; - return ( - - {this.renderDeleteButton()} - - - - ); - } - - 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; - } - } + const search = { + toolsRight: this.renderDeleteButton(), }; 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.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.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; - } } } From 983418e83a6e5175ce3799130d984db3e6a406b3 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 17 Mar 2020 22:56:58 -0700 Subject: [PATCH 10/10] fix types errors --- .../reporting/public/components/buttons/index.tsx | 3 --- .../public/components/buttons/report_error_button.tsx | 11 +++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/buttons/index.tsx b/x-pack/plugins/reporting/public/components/buttons/index.tsx index 3c8f34f1d1a8af..cfa19ba711108c 100644 --- a/x-pack/plugins/reporting/public/components/buttons/index.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/index.tsx @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { JobStatuses } from '../../../constants'; -import { Job, Props as ListingProps } from '../report_listing'; export { ReportErrorButton } from './report_error_button'; export { ReportDeleteButton } from './report_delete_button'; export { ReportDownloadButton } from './report_download_button'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 897295d595f970..1e33cc0188b8c2 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -9,11 +9,12 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; 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 { @@ -40,7 +41,7 @@ class ReportErrorButtonUi extends Component { } public render() { - const { record, apiClient, intl } = this.props; + const { record, intl } = this.props; if (record.status !== JobStatuses.FAILED) { return null; @@ -96,9 +97,11 @@ class ReportErrorButtonUi extends Component { }; 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 }); } @@ -106,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', }),