From 063604b15dc75b33f21304c8d502b29ff88f26ac Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 15 Nov 2021 14:19:26 +0100 Subject: [PATCH] [Reporting] Add link to Kibana app from Reporting management UI + Design update (#111412) * moved components to nested components dir * added health status indicator * download button -> download link * updated poblic Job API, remove some of the "rendering" behaviour * restructure list table contents and clean up use of i18n * set table column widths * slight update to table column widths * actually use action width :facepalm: * added view in app link component and included space id in public side Job * server side changes so that we can get the job payload containing the locator * initial round of public-side changes to make the link to Kibana app work * added tooltip to view action * remove unused import and do not show chrome * removed use of fp-ts * added type column and updated mobile look * remove unused imports * take a different approach to job query factory -> added new function called "getReport" and leave "get" as is * update i18n * code simplifications, also ensure that "PROCESSING" status is being handled by health indicator * do not hide chrome * refactor jest test: - make test more specific and remove snapshot - added use of isMounted() to not run set state when component is not mounted * surface deprecation warning in a special way * updated one functional test * updated other functional test * Several updates to bring table more in line with design * Removed "created by" column * Added app icons instead of names * Added content type indication (PDF, CSV or PNG) * Updated the "info" button to have no colors * Updated the status to have a timestamp and show "yellow" if we detect any issues and guide users to view the report info. * a lot of changes to bring this more in line with defazio designs * fix lint * -wip- [skip-ci] * some very basic house keeping [skip-ci] * get to a point where the linking behaviour is working as expected * further house-keeping, remove unecessary components * clean up imports * move hasIssues check into status indicator * refactored report status indicator * hide open kibana app button when not available * remove unused import * fix jest tests * created a new redirect plugin to avoid page flicker * remove unused report info button * removed unused translations * fix jest tests after changing the redirect app path * added reportingRedirect to applicationUsageSchema * added column width for type * update test for extracting first row title * update functional test snapshot * updated plugins schema * removed the interstitial page so that we do not conflict with future work planned for the share service * remove unused i18n * small, but center-ish type icons * elastic@ email address * add i18n, update import with forward slash and added missing ":" to TODO * move non-type export to own import line and "type" to only-type imports * remove unecessary export * refactor payload endpoint to locatorParams endpoint and document query function * finish refactoring client side to work with new locatorParams endpoint * remove unused import * use info endpoint because it contains payload! * added functional test to ensure that we can navigate back to report * added jest test for checking that link navigated to is spaces aware * fix type issue and remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../collectors/application_usage/schema.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 131 + x-pack/plugins/reporting/common/constants.ts | 2 +- x-pack/plugins/reporting/common/job_utils.ts | 25 + x-pack/plugins/reporting/common/types/base.ts | 11 + .../plugins/reporting/common/types/index.ts | 4 +- x-pack/plugins/reporting/common/types/url.ts | 4 +- x-pack/plugins/reporting/public/constants.ts | 8 - x-pack/plugins/reporting/public/lib/job.tsx | 48 +- .../reporting_api_client.ts | 15 + .../report_info_button.test.tsx.snap | 256 - .../report_listing.test.tsx.snap | 10738 ---------------- .../{ => components}/ilm_policy_link.tsx | 4 +- .../public/management/components/index.ts | 13 + .../ilm_policy_migration_needed_callout.tsx | 8 +- .../migrate_ilm_policy_callout/index.tsx | 2 +- .../{ => components}/report_delete_button.tsx | 51 +- .../{ => components}/report_diagnostic.tsx | 2 +- .../components/report_info_button.tsx | 52 + .../components/report_info_flyout.tsx | 93 + .../components/report_info_flyout_content.tsx | 193 + .../components/report_status_indicator.tsx | 101 + .../reporting/public/management/index.ts | 2 - .../management/report_download_button.tsx | 65 - .../management/report_info_button.test.tsx | 81 - .../public/management/report_info_button.tsx | 377 - .../public/management/report_listing.scss | 7 + .../public/management/report_listing.test.tsx | 58 +- .../public/management/report_listing.tsx | 249 +- .../reporting/public/management/utils.ts | 38 + x-pack/plugins/reporting/public/plugin.ts | 24 +- .../public/redirect/mount_redirect_app.tsx | 5 +- .../public/redirect/redirect_app.scss | 8 + .../public/redirect/redirect_app.tsx | 73 +- .../reporting/public/shared_imports.ts | 2 + x-pack/plugins/reporting/public/utils.ts | 12 - .../v2/get_full_redirect_app_url.test.ts | 4 +- .../export_types/png_v2/execute_job.test.ts | 2 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../reporting_management/report_listing.ts | 60 +- .../reporting_and_security/management.ts | 24 +- .../reporting_without_security/management.ts | 7 +- 43 files changed, 1114 insertions(+), 11754 deletions(-) delete mode 100644 x-pack/plugins/reporting/public/constants.ts delete mode 100644 x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap delete mode 100644 x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap rename x-pack/plugins/reporting/public/management/{ => components}/ilm_policy_link.tsx (90%) create mode 100644 x-pack/plugins/reporting/public/management/components/index.ts rename x-pack/plugins/reporting/public/management/{ => components}/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx (90%) rename x-pack/plugins/reporting/public/management/{ => components}/migrate_ilm_policy_callout/index.tsx (92%) rename x-pack/plugins/reporting/public/management/{ => components}/report_delete_button.tsx (59%) rename x-pack/plugins/reporting/public/management/{ => components}/report_diagnostic.tsx (98%) create mode 100644 x-pack/plugins/reporting/public/management/components/report_info_button.tsx create mode 100644 x-pack/plugins/reporting/public/management/components/report_info_flyout.tsx create mode 100644 x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx create mode 100644 x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx delete mode 100644 x-pack/plugins/reporting/public/management/report_download_button.tsx delete mode 100644 x-pack/plugins/reporting/public/management/report_info_button.test.tsx delete mode 100644 x-pack/plugins/reporting/public/management/report_info_button.tsx create mode 100644 x-pack/plugins/reporting/public/management/report_listing.scss create mode 100644 x-pack/plugins/reporting/public/management/utils.ts create mode 100644 x-pack/plugins/reporting/public/redirect/redirect_app.scss delete mode 100644 x-pack/plugins/reporting/public/utils.ts diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 7c112083875d15..adfe8da335a148 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -150,6 +150,7 @@ export const applicationUsageSchema = { 'observability-overview': commonSchema, osquery: commonSchema, security_account: commonSchema, + reportingRedirect: commonSchema, security_access_agreement: commonSchema, security_capture_url: commonSchema, // It's a forward app so we'll likely never report it security_logged_out: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 21273482dd2b88..60c5bbd4346ec4 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4232,6 +4232,137 @@ } } }, + "reportingRedirect": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "security_access_agreement": { "properties": { "appId": { diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index cafab65677ee4d..1eef032945e69d 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -121,7 +121,7 @@ export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATO * be injected to the page */ export const getRedirectAppPath = () => { - return '/app/management/insightsAndAlerting/reporting/r'; + return '/app/reportingRedirect'; }; // Statuses diff --git a/x-pack/plugins/reporting/common/job_utils.ts b/x-pack/plugins/reporting/common/job_utils.ts index d8b4503cfefbaf..c1fd2eb5eb8c45 100644 --- a/x-pack/plugins/reporting/common/job_utils.ts +++ b/x-pack/plugins/reporting/common/job_utils.ts @@ -5,7 +5,32 @@ * 2.0. */ +import { + CSV_JOB_TYPE, + PDF_JOB_TYPE, + PNG_JOB_TYPE, + PDF_JOB_TYPE_V2, + PNG_JOB_TYPE_V2, + CSV_JOB_TYPE_DEPRECATED, +} from './constants'; + // TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy // export type entirely export const isJobV2Params = ({ sharingData }: { sharingData: Record }): boolean => sharingData.locatorParams != null; + +export const prettyPrintJobType = (type: string) => { + switch (type) { + case PDF_JOB_TYPE: + case PDF_JOB_TYPE_V2: + return 'PDF'; + case CSV_JOB_TYPE: + case CSV_JOB_TYPE_DEPRECATED: + return 'CSV'; + case PNG_JOB_TYPE: + case PNG_JOB_TYPE_V2: + return 'PNG'; + default: + return type; + } +}; diff --git a/x-pack/plugins/reporting/common/types/base.ts b/x-pack/plugins/reporting/common/types/base.ts index 44960c57f61c14..a44378979ac3ca 100644 --- a/x-pack/plugins/reporting/common/types/base.ts +++ b/x-pack/plugins/reporting/common/types/base.ts @@ -7,6 +7,7 @@ import type { Ensure, SerializableRecord } from '@kbn/utility-types'; import type { LayoutParams } from './layout'; +import { LocatorParams } from './url'; export type JobId = string; @@ -21,9 +22,19 @@ export type BaseParams = Ensure< SerializableRecord >; +export type BaseParamsV2 = BaseParams & { + locatorParams: LocatorParams[]; +}; + // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { headers: string; spaceId?: string; isDeprecated?: boolean; } + +export interface BasePayloadV2 extends BaseParamsV2 { + headers: string; + spaceId?: string; + isDeprecated?: boolean; +} diff --git a/x-pack/plugins/reporting/common/types/index.ts b/x-pack/plugins/reporting/common/types/index.ts index 75e8cb0af9698e..8612400e8b3904 100644 --- a/x-pack/plugins/reporting/common/types/index.ts +++ b/x-pack/plugins/reporting/common/types/index.ts @@ -6,9 +6,9 @@ */ import type { Size, LayoutParams } from './layout'; -import type { JobId, BaseParams, BasePayload } from './base'; +import type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 } from './base'; -export type { JobId, BaseParams, BasePayload }; +export type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 }; export type { Size, LayoutParams }; export type { DownloadReportFn, diff --git a/x-pack/plugins/reporting/common/types/url.ts b/x-pack/plugins/reporting/common/types/url.ts index dfb8ee9f908e3c..28e935713c45e4 100644 --- a/x-pack/plugins/reporting/common/types/url.ts +++ b/x-pack/plugins/reporting/common/types/url.ts @@ -14,9 +14,7 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; -export interface LocatorParams< - P extends SerializableRecord = SerializableRecord & { forceNow?: string } -> { +export interface LocatorParams

{ id: string; version: string; params: P; diff --git a/x-pack/plugins/reporting/public/constants.ts b/x-pack/plugins/reporting/public/constants.ts deleted file mode 100644 index c7e77fd44a7804..00000000000000 --- a/x-pack/plugins/reporting/public/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const REACT_ROUTER_REDIRECT_APP_PATH = '/r'; diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx index d5d0695aaefb96..d5d77ac18aa5cc 100644 --- a/x-pack/plugins/reporting/public/lib/job.tsx +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -12,6 +12,7 @@ import moment from 'moment'; import React from 'react'; import { JOB_STATUSES } from '../../common/constants'; import { + BaseParamsV2, JobId, ReportApiJSON, ReportOutput, @@ -34,6 +35,7 @@ export class Job { public objectType: ReportPayload['objectType']; public title: ReportPayload['title']; public isDeprecated: ReportPayload['isDeprecated']; + public spaceId: ReportPayload['spaceId']; public browserTimezone?: ReportPayload['browserTimezone']; public layout: ReportPayload['layout']; @@ -57,6 +59,8 @@ export class Job { public max_size_reached?: TaskRunResult['max_size_reached']; public warnings?: TaskRunResult['warnings']; + public locatorParams?: BaseParamsV2['locatorParams']; + constructor(report: ReportApiJSON) { this.id = report.id; this.index = report.index; @@ -82,9 +86,11 @@ export class Job { this.content_type = report.output?.content_type; this.isDeprecated = report.payload.isDeprecated || false; + this.spaceId = report.payload.spaceId; this.csv_contains_formulas = report.output?.csv_contains_formulas; this.max_size_reached = report.output?.max_size_reached; this.warnings = report.output?.warnings; + this.locatorParams = (report.payload as BaseParamsV2).locatorParams; } getStatusMessage() { @@ -167,6 +173,25 @@ export class Job { ); } + /** + * Returns a user friendly version of the report job creation date + */ + getCreatedAtDate(): string { + return this.formatDate(this.created_at); + } + + /** + * Returns a user friendly version of the user that created the report job + */ + getCreatedBy(): string { + return ( + this.created_by || + i18n.translate('xpack.reporting.jobCreatedBy.unknownUserPlaceholderText', { + defaultMessage: 'Unknown', + }) + ); + } + getCreatedAtLabel() { if (this.created_by) { return ( @@ -191,15 +216,20 @@ export class Job { } } + getDeprecatedMessage(): undefined | string { + if (this.isDeprecated) { + return i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { + defaultMessage: + 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', + }); + } + } + getWarnings() { const warnings: string[] = []; - if (this.isDeprecated) { - warnings.push( - i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { - defaultMessage: - 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', - }) - ); + const deprecatedMessage = this.getDeprecatedMessage(); + if (deprecatedMessage) { + warnings.push(deprecatedMessage); } if (this.csv_contains_formulas) { @@ -234,6 +264,10 @@ export class Job { } } + getPrettyStatusTimestamp() { + return this.formatDate(this.getStatusTimestamp()); + } + private formatDate(timestamp: string) { try { return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index b27c2a65be963c..c44427f3ca9e1c 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -10,12 +10,14 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import type { HttpFetchQuery } from 'src/core/public'; import { HttpSetup, IUiSettingsClient } from 'src/core/public'; +import { buildKibanaPath } from '../../../common/build_kibana_path'; import { API_BASE_GENERATE, API_BASE_URL, API_GENERATE_IMMEDIATE, API_LIST_URL, API_MIGRATE_ILM_POLICY_URL, + getRedirectAppPath, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; import { @@ -73,6 +75,19 @@ export class ReportingAPIClient implements IReportingAPI { private kibanaVersion: string ) {} + public getKibanaAppHref(job: Job): string { + const searchParams = stringify({ jobId: job.id }); + + const path = buildKibanaPath({ + basePath: this.http.basePath.serverBasePath, + spaceId: job.spaceId, + appPath: getRedirectAppPath(), + }); + + const href = `${path}?${searchParams}`; + return href; + } + public getReportURL(jobId: string) { const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); const downloadLink = `${apiBaseUrl}/download/${jobId}`; diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap deleted file mode 100644 index e8b9362db75250..00000000000000 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap +++ /dev/null @@ -1,256 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReportInfoButton handles button click flyout on click 1`] = ` - -`; - -exports[`ReportInfoButton opens flyout with fetch error info 1`] = ` -Array [ - -

-
- , -
-
, -] -`; - -exports[`ReportInfoButton opens flyout with info 1`] = ` -Array [ - -
- - - - - - - - - - - - - - - - -
- - - - - - - - -`; diff --git a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx similarity index 90% rename from x-pack/plugins/reporting/public/management/ilm_policy_link.tsx rename to x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx index 1dccb11dbbbc54..dfb884c24e917f 100644 --- a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx +++ b/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx @@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; import type { ApplicationStart } from 'src/core/public'; -import { ILM_POLICY_NAME } from '../../common/constants'; -import { LocatorPublic, SerializableRecord } from '../shared_imports'; +import { ILM_POLICY_NAME } from '../../../common/constants'; +import { LocatorPublic, SerializableRecord } from '../../shared_imports'; interface Props { navigateToUrl: ApplicationStart['navigateToUrl']; diff --git a/x-pack/plugins/reporting/public/management/components/index.ts b/x-pack/plugins/reporting/public/management/components/index.ts new file mode 100644 index 00000000000000..10c34ed628a15f --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +export { IlmPolicyLink } from './ilm_policy_link'; +export { ReportDeleteButton } from './report_delete_button'; +export { ReportDiagnostic } from './report_diagnostic'; +export { ReportStatusIndicator } from './report_status_indicator'; +export { ReportInfoFlyout } from './report_info_flyout'; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx similarity index 90% rename from x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx rename to x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx index e96cb842d55cf3..7eb54049a15a9f 100644 --- a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx +++ b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx @@ -9,13 +9,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { FunctionComponent } from 'react'; import React, { useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; import { EuiCallOut, EuiButton, EuiCode } from '@elastic/eui'; import type { NotificationsSetup } from 'src/core/public'; -import { ILM_POLICY_NAME } from '../../../common/constants'; +import { ILM_POLICY_NAME } from '../../../../common/constants'; -import { useInternalApiClient } from '../../lib/reporting_api_client'; +import { useInternalApiClient } from '../../../lib/reporting_api_client'; const i18nTexts = { title: i18n.translate('xpack.reporting.listing.ilmPolicyCallout.migrationNeededTitle', { @@ -63,6 +64,7 @@ export const IlmPolicyMigrationNeededCallOut: FunctionComponent = ({ onMigrationDone, }) => { const [isMigratingIndices, setIsMigratingIndices] = useState(false); + const isMounted = useMountedState(); const { apiClient } = useInternalApiClient(); @@ -78,7 +80,7 @@ export const IlmPolicyMigrationNeededCallOut: FunctionComponent = ({ toastMessage: e.body?.message, }); } finally { - setIsMigratingIndices(false); + if (isMounted()) setIsMigratingIndices(false); } }; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx similarity index 92% rename from x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx rename to x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx index 892cbcdde5edee..0c2359acdb6799 100644 --- a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx +++ b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx @@ -11,7 +11,7 @@ import { EuiSpacer, EuiFlexItem } from '@elastic/eui'; import { NotificationsSetup } from 'src/core/public'; -import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; +import { useIlmPolicyStatus } from '../../../lib/ilm_policy_status_context'; import { IlmPolicyMigrationNeededCallOut } from './ilm_policy_migration_needed_callout'; diff --git a/x-pack/plugins/reporting/public/management/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/components/report_delete_button.tsx similarity index 59% rename from x-pack/plugins/reporting/public/management/report_delete_button.tsx rename to x-pack/plugins/reporting/public/management/components/report_delete_button.tsx index da1ce9dd9e1cb6..d91560ddd86f5c 100644 --- a/x-pack/plugins/reporting/public/management/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/management/components/report_delete_button.tsx @@ -6,9 +6,10 @@ */ import { EuiButton, EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React, { Fragment, PureComponent } from 'react'; -import { Job } from '../lib/job'; -import { ListingProps } from './'; +import { Job } from '../../lib/job'; +import { ListingProps } from '../'; type DeleteFn = () => Promise; type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; @@ -31,34 +32,25 @@ export class ReportDeleteButton extends PureComponent { } private renderConfirm() { - const { intl, jobsToDelete } = this.props; + const { 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].title } - ); - const message = intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteConfirmMessage', + ? i18n.translate('xpack.reporting.listing.table.deleteNumConfirmTitle', { + defaultMessage: `Delete {num} reports?`, + values: { num: jobsToDelete.length }, + }) + : i18n.translate('xpack.reporting.listing.table.deleteConfirmTitle', { + defaultMessage: `Delete the "{name}" report?`, + values: { name: jobsToDelete[0].title }, + }); + const message = i18n.translate('xpack.reporting.listing.table.deleteConfirmMessage', { defaultMessage: `You can't recover deleted reports.`, }); - const confirmButtonText = intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteConfirmButton', + const confirmButtonText = i18n.translate('xpack.reporting.listing.table.deleteConfirmButton', { defaultMessage: `Delete`, }); - const cancelButtonText = intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteCancelButton', + const cancelButtonText = i18n.translate('xpack.reporting.listing.table.deleteCancelButton', { defaultMessage: `Cancel`, }); @@ -78,7 +70,7 @@ export class ReportDeleteButton extends PureComponent { } public render() { - const { jobsToDelete, intl } = this.props; + const { jobsToDelete } = this.props; if (jobsToDelete.length === 0) return null; return ( @@ -89,13 +81,10 @@ export class ReportDeleteButton extends PureComponent { color={'danger'} data-test-subj="deleteReportButton" > - {intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteReportButton', - defaultMessage: `Delete {num, plural, one {report} other {reports} }`, - }, - { num: jobsToDelete.length } - )} + {i18n.translate('xpack.reporting.listing.table.deleteReportButton', { + defaultMessage: `Delete {num, plural, one {report} other {reports} }`, + values: { num: jobsToDelete.length }, + })} {this.state.showConfirm ? this.renderConfirm() : null} diff --git a/x-pack/plugins/reporting/public/management/report_diagnostic.tsx b/x-pack/plugins/reporting/public/management/components/report_diagnostic.tsx similarity index 98% rename from x-pack/plugins/reporting/public/management/report_diagnostic.tsx rename to x-pack/plugins/reporting/public/management/components/report_diagnostic.tsx index ce585fe427e6c4..124ce9af891a39 100644 --- a/x-pack/plugins/reporting/public/management/report_diagnostic.tsx +++ b/x-pack/plugins/reporting/public/management/components/report_diagnostic.tsx @@ -21,7 +21,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { ReportingAPIClient, DiagnoseResponse } from '../lib/reporting_api_client'; +import { ReportingAPIClient, DiagnoseResponse } from '../../lib/reporting_api_client'; interface Props { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_button.tsx b/x-pack/plugins/reporting/public/management/components/report_info_button.tsx new file mode 100644 index 00000000000000..26e495e5908dc6 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_info_button.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { Job } from '../../lib/job'; + +interface Props { + job: Job; + onClick: () => void; +} + +export const ReportInfoButton: FunctionComponent = ({ job, onClick }) => { + let message = i18n.translate('xpack.reporting.listing.table.reportInfoButtonTooltip', { + defaultMessage: 'See report info.', + }); + if (job.getError()) { + message = i18n.translate('xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', { + defaultMessage: 'See report info and error message.', + }); + } else if (job.getWarnings()) { + message = i18n.translate('xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', { + defaultMessage: 'See report info and warnings.', + }); + } + + const showReportInfoCopy = i18n.translate( + 'xpack.reporting.listing.table.showReportInfoAriaLabel', + { + defaultMessage: 'Show report info', + } + ); + + return ( + + + {showReportInfoCopy} + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_flyout.tsx b/x-pack/plugins/reporting/public/management/components/report_info_flyout.tsx new file mode 100644 index 00000000000000..2a7d52cf9403b7 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_info_flyout.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState, useEffect } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiText, + EuiTitle, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Job } from '../../lib/job'; +import { useInternalApiClient } from '../../lib/reporting_api_client'; + +import { ReportInfoFlyoutContent } from './report_info_flyout_content'; + +interface Props { + onClose: () => void; + job: Job; +} + +export const ReportInfoFlyout: FunctionComponent = ({ onClose, job }) => { + const [isLoading, setIsLoading] = useState(true); + const [loadingError, setLoadingError] = useState(); + const [info, setInfo] = useState(); + const isMounted = useMountedState(); + const { apiClient } = useInternalApiClient(); + + useEffect(() => { + (async function loadInfo() { + if (isLoading) { + try { + const infoResponse = await apiClient.getInfo(job.id); + if (isMounted()) { + setInfo(infoResponse); + } + } catch (err) { + if (isMounted()) { + setLoadingError(err); + } + } finally { + if (isMounted()) { + setIsLoading(false); + } + } + } + })(); + }, [isLoading, apiClient, job.id, isMounted]); + + return ( + + + + +

+ {loadingError + ? i18n.translate('xpack.reporting.listing.table.reportInfoUnableToFetch', { + defaultMessage: 'Unable to fetch report info.', + }) + : i18n.translate('xpack.reporting.listing.table.reportCalloutTitle', { + defaultMessage: 'Report info', + })} +

+
+
+ + {isLoading ? ( + + ) : loadingError ? undefined : !!info ? ( + + + + ) : undefined} + +
+
+ ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx b/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx new file mode 100644 index 00000000000000..25199c4abaa683 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiDescriptionList, EuiSpacer, EuiText } from '@elastic/eui'; + +import type { Job } from '../../lib/job'; +import { USES_HEADLESS_JOB_TYPES } from '../../../common/constants'; + +const NA = i18n.translate('xpack.reporting.listing.infoPanel.notApplicableLabel', { + defaultMessage: 'N/A', +}); + +const UNKNOWN = i18n.translate('xpack.reporting.listing.infoPanel.unknownLabel', { + defaultMessage: 'unknown', +}); + +const getDimensions = (info: Job): string => { + const defaultDimensions = { width: null, height: null }; + const { width, height } = info.layout?.dimensions || defaultDimensions; + if (width && height) { + return `Width: ${width} x Height: ${height}`; + } + return UNKNOWN; +}; + +interface Props { + info: Job; +} + +export const ReportInfoFlyoutContent: FunctionComponent = ({ info }) => { + const timeout = info.timeout ? info.timeout.toString() : NA; + + const jobInfo = [ + { + title: i18n.translate('xpack.reporting.listing.infoPanel.titleInfo', { + defaultMessage: 'Title', + }), + description: info.title || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.createdAtInfo', { + defaultMessage: 'Created at', + }), + description: info.getCreatedAtLabel(), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.statusInfo', { + defaultMessage: 'Status', + }), + description: info.getStatus(), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.tzInfo', { + defaultMessage: 'Time zone', + }), + description: info.browserTimezone || NA, + }, + ]; + + const processingInfo = [ + { + title: i18n.translate('xpack.reporting.listing.infoPanel.startedAtInfo', { + defaultMessage: 'Started at', + }), + description: info.started_at || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.completedAtInfo', { + defaultMessage: 'Completed at', + }), + description: info.completed_at || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.processedByInfo', { + defaultMessage: 'Processed by', + }), + description: + info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.contentTypeInfo', { + defaultMessage: 'Content type', + }), + description: info.content_type || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.sizeInfo', { + defaultMessage: 'Size in bytes', + }), + description: info.size?.toString() || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.attemptsInfo', { + defaultMessage: 'Attempts', + }), + description: info.attempts.toString(), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.maxAttemptsInfo', { + defaultMessage: 'Max attempts', + }), + description: info.max_attempts?.toString() || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.timeoutInfo', { + defaultMessage: 'Timeout', + }), + description: timeout, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.exportTypeInfo', { + defaultMessage: 'Export type', + }), + description: info.isDeprecated + ? i18n.translate('xpack.reporting.listing.table.reportCalloutExportTypeDeprecated', { + defaultMessage: '{jobtype} (DEPRECATED)', + values: { jobtype: info.jobtype }, + }) + : info.jobtype, + }, + + // TODO: when https://github.com/elastic/kibana/pull/106137 is merged, add kibana version field + ]; + + const jobScreenshot = [ + { + title: i18n.translate('xpack.reporting.listing.infoPanel.dimensionsInfo', { + defaultMessage: 'Dimensions', + }), + description: getDimensions(info), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.layoutInfo', { + defaultMessage: 'Layout', + }), + description: info.layout?.id || UNKNOWN, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.browserTypeInfo', { + defaultMessage: 'Browser type', + }), + description: info.browser_type || NA, + }, + ]; + + const warnings = info.getWarnings(); + const warningsInfo = warnings && [ + { + title: Warnings, + description: {warnings}, + }, + ]; + + const errored = info.getError(); + const errorInfo = errored && [ + { + title: Error, + description: {errored}, + }, + ]; + + return ( + <> + + + + {USES_HEADLESS_JOB_TYPES.includes(info.jobtype) ? ( + <> + + + + ) : null} + {warningsInfo ? ( + <> + + + + ) : null} + {errorInfo ? ( + <> + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx b/x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx new file mode 100644 index 00000000000000..21fd0fc76745cd --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; + +import type { Job } from '../../lib/job'; +import { JOB_STATUSES } from '../../../common/constants'; +import { jobHasIssues } from '../utils'; + +interface Props { + job: Job; +} + +const i18nTexts = { + completed: i18n.translate('xpack.reporting.statusIndicator.completedLabel', { + defaultMessage: 'Done', + }), + completedWithWarnings: i18n.translate( + 'xpack.reporting.statusIndicator.completedWithWarningsLabel', + { + defaultMessage: 'Done, warnings detected', + } + ), + pending: i18n.translate('xpack.reporting.statusIndicator.pendingLabel', { + defaultMessage: 'Pending', + }), + processing: ({ attempt, of }: { attempt: number; of?: number }) => + of !== undefined + ? i18n.translate('xpack.reporting.statusIndicator.processingMaxAttemptsLabel', { + defaultMessage: `Processing, attempt {attempt} of {of}`, + values: { attempt, of }, + }) + : i18n.translate('xpack.reporting.statusIndicator.processingLabel', { + defaultMessage: `Processing, attempt {attempt}`, + values: { attempt }, + }), + failed: i18n.translate('xpack.reporting.statusIndicator.failedLabel', { + defaultMessage: 'Failed', + }), + unknown: i18n.translate('xpack.reporting.statusIndicator.unknownLabel', { + defaultMessage: 'Unknown', + }), + lastStatusUpdate: ({ date }: { date: string }) => + i18n.translate('xpack.reporting.statusIndicator.lastStatusUpdateLabel', { + defaultMessage: 'Updated at {date}', + values: { date }, + }), +}; + +export const ReportStatusIndicator: FC = ({ job }) => { + const hasIssues = useMemo(() => jobHasIssues(job), [job]); + + let icon: JSX.Element; + let statusText: string; + + switch (job.status) { + case JOB_STATUSES.COMPLETED: + if (hasIssues) { + icon = ; + statusText = i18nTexts.completedWithWarnings; + break; + } + icon = ; + statusText = i18nTexts.completed; + break; + case JOB_STATUSES.WARNINGS: + icon = ; + statusText = i18nTexts.completedWithWarnings; + break; + case JOB_STATUSES.PENDING: + icon = ; + statusText = i18nTexts.pending; + break; + case JOB_STATUSES.PROCESSING: + icon = ; + statusText = i18nTexts.processing({ attempt: job.attempts, of: job.max_attempts }); + break; + case JOB_STATUSES.FAILED: + icon = ; + statusText = i18nTexts.failed; + break; + default: + icon = ; + statusText = i18nTexts.unknown; + } + + return ( + + + {icon} + {statusText} + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/index.ts b/x-pack/plugins/reporting/public/management/index.ts index 4d324135288dbc..7bd7845051d2c8 100644 --- a/x-pack/plugins/reporting/public/management/index.ts +++ b/x-pack/plugins/reporting/public/management/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { InjectedIntl } from '@kbn/i18n/react'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; @@ -14,7 +13,6 @@ import { ClientConfigType } from '../plugin'; import type { SharePluginSetup } from '../shared_imports'; export interface ListingProps { - intl: InjectedIntl; apiClient: ReportingAPIClient; capabilities: ApplicationStart['capabilities']; license$: LicensingPluginSetup['license$']; // FIXME: license$ is deprecated diff --git a/x-pack/plugins/reporting/public/management/report_download_button.tsx b/x-pack/plugins/reporting/public/management/report_download_button.tsx deleted file mode 100644 index f21c83fbf42da5..00000000000000 --- a/x-pack/plugins/reporting/public/management/report_download_button.tsx +++ /dev/null @@ -1,65 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { InjectedIntl } from '@kbn/i18n/react'; -import React, { FunctionComponent } from 'react'; -import { JOB_STATUSES } from '../../common/constants'; -import { Job as ListingJob } from '../lib/job'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; - -interface Props { - intl: InjectedIntl; - apiClient: ReportingAPIClient; - job: ListingJob; -} - -export const ReportDownloadButton: FunctionComponent = (props: Props) => { - const { job, apiClient, intl } = props; - - if (job.status !== JOB_STATUSES.COMPLETED && job.status !== JOB_STATUSES.WARNINGS) { - return null; - } - - const button = ( - apiClient.downloadReport(job.id)} - iconType="importAction" - aria-label={intl.formatMessage({ - id: 'xpack.reporting.listing.table.downloadReportAriaLabel', - defaultMessage: 'Download report', - })} - /> - ); - - const warnings = job.getWarnings(); - if (warnings) { - return ( - - {button} - - ); - } - - return ( - - {button} - - ); -}; diff --git a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx deleted file mode 100644 index c52027355ac5ef..00000000000000 --- a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx +++ /dev/null @@ -1,81 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import { coreMock } from '../../../../../src/core/public/mocks'; -import { Job } from '../lib/job'; -import { ReportInfoButton } from './report_info_button'; - -jest.mock('../lib/reporting_api_client'); - -import { ReportingAPIClient } from '../lib/reporting_api_client'; - -const coreSetup = coreMock.createSetup(); -const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); - -const job = new Job({ - id: 'abc-123', - index: '.reporting-2020.04.12', - migration_version: '7.15.0', - attempts: 0, - browser_type: 'chromium', - created_at: '2020-04-14T21:01:13.064Z', - created_by: 'elastic', - jobtype: 'printable_pdf', - max_attempts: 1, - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - payload: { - browserTimezone: 'America/Phoenix', - version: '7.15.0-test', - layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - process_expiration: '1970-01-01T00:00:00.000Z', - status: 'pending', - timeout: 300000, -}); - -describe('ReportInfoButton', () => { - it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - expect(input).toMatchSnapshot(); - }); - - it('opens flyout with info', async () => { - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - - input.simulate('click'); - - const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); - expect(flyout).toMatchSnapshot(); - - expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); - }); - - it('opens flyout with fetch error info', () => { - // simulate fetch failure - apiClient.getInfo = jest.fn(() => { - throw new Error('Could not fetch the job info'); - }); - - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - - input.simulate('click'); - - const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); - expect(flyout).toMatchSnapshot(); - - expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); - }); -}); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.tsx b/x-pack/plugins/reporting/public/management/report_info_button.tsx deleted file mode 100644 index 7a70286785e4f1..00000000000000 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ /dev/null @@ -1,377 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiDescriptionList, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPortal, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; -import { Job } from '../lib/job'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { ListingProps } from '.'; - -interface Props extends Pick { - apiClient: ReportingAPIClient; - job: Job; -} - -interface State { - isLoading: boolean; - isFlyoutVisible: boolean; - calloutTitle: string; - info: Job | null; - error: Error | null; -} - -const NA = 'n/a'; -const UNKNOWN = 'unknown'; - -const getDimensions = (info: Job): string => { - const defaultDimensions = { width: null, height: null }; - const { width, height } = info.layout?.dimensions || defaultDimensions; - if (width && height) { - return `Width: ${width} x Height: ${height}`; - } - return UNKNOWN; -}; - -class ReportInfoButtonUi extends Component { - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isFlyoutVisible: false, - calloutTitle: props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportCalloutTitle', - defaultMessage: 'Report info', - }), - info: null, - error: null, - }; - - this.closeFlyout = this.closeFlyout.bind(this); - this.showFlyout = this.showFlyout.bind(this); - } - - public renderInfo() { - const { info, error: err } = this.state; - if (err) { - return err.message; - } - if (!info) { - return null; - } - - const timeout = info.timeout ? info.timeout.toString() : NA; - - const jobInfo = [ - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.titleInfo', - defaultMessage: 'Title', - }), - description: info.title || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.createdAtInfo', - defaultMessage: 'Created at', - }), - description: info.getCreatedAtLabel(), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.statusInfo', - defaultMessage: 'Status', - }), - description: info.getStatus(), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.tzInfo', - defaultMessage: 'Time zone', - }), - description: info.browserTimezone || NA, - }, - ]; - - const processingInfo = [ - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.startedAtInfo', - defaultMessage: 'Started at', - }), - description: info.started_at || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.completedAtInfo', - defaultMessage: 'Completed at', - }), - description: info.completed_at || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.processedByInfo', - defaultMessage: 'Processed by', - }), - description: - info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.contentTypeInfo', - defaultMessage: 'Content type', - }), - description: info.content_type || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.sizeInfo', - defaultMessage: 'Size in bytes', - }), - description: info.size?.toString() || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.attemptsInfo', - defaultMessage: 'Attempts', - }), - description: info.attempts.toString(), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.maxAttemptsInfo', - defaultMessage: 'Max attempts', - }), - description: info.max_attempts?.toString() || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.timeoutInfo', - defaultMessage: 'Timeout', - }), - description: timeout, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.exportTypeInfo', - defaultMessage: 'Export type', - }), - description: info.isDeprecated - ? this.props.intl.formatMessage( - { - id: 'xpack.reporting.listing.table.reportCalloutExportTypeDeprecated', - defaultMessage: '{jobtype} (DEPRECATED)', - }, - { jobtype: info.jobtype } - ) - : info.jobtype, - }, - - // TODO when https://github.com/elastic/kibana/pull/106137 is merged, add kibana version field - ]; - - const jobScreenshot = [ - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.dimensionsInfo', - defaultMessage: 'Dimensions', - }), - description: getDimensions(info), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.layoutInfo', - defaultMessage: 'Layout', - }), - description: info.layout?.id || UNKNOWN, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.browserTypeInfo', - defaultMessage: 'Browser type', - }), - description: info.browser_type || NA, - }, - ]; - - const warnings = info.getWarnings(); - const warningsInfo = warnings && [ - { - title: Warnings, - description: {warnings}, - }, - ]; - - const errored = info.getError(); - const errorInfo = errored && [ - { - title: Error, - description: {errored}, - }, - ]; - - return ( - <> - - - - {USES_HEADLESS_JOB_TYPES.includes(info.jobtype) ? ( - <> - - - - ) : null} - {warningsInfo ? ( - <> - - - - ) : null} - {errorInfo ? ( - <> - - - - ) : null} - - ); - } - - public componentWillUnmount() { - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - } - - public render() { - const job = this.props.job; - let flyout; - - if (this.state.isFlyoutVisible) { - flyout = ( - - - - -

{this.state.calloutTitle}

-
-
- - {this.renderInfo()} - -
-
- ); - } - - let message = this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoButtonTooltip', - defaultMessage: 'See report info.', - }); - if (job.getError()) { - message = this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', - defaultMessage: 'See report info and error message.', - }); - } else if (job.getWarnings()) { - message = this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', - defaultMessage: 'See report info and warnings.', - }); - } - - let buttonIconType = 'iInCircle'; - let buttonColor: 'primary' | 'danger' | 'warning' = 'primary'; - if (job.getWarnings() || job.getError()) { - buttonIconType = 'alert'; - buttonColor = 'danger'; - } - if (job.getWarnings()) { - buttonColor = 'warning'; - } - - return ( - <> - - - - {flyout} - - ); - } - - private loadInfo = async () => { - this.setState({ isLoading: true }); - try { - const info = await this.props.apiClient.getInfo(this.props.job.id); - if (this.mounted) { - this.setState({ isLoading: false, info }); - } - } catch (err) { - if (this.mounted) { - this.setState({ - isLoading: false, - calloutTitle: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoUnableToFetch', - defaultMessage: 'Unable to fetch report info.', - }), - info: null, - error: err, - }); - } - } - }; - - private closeFlyout = () => { - this.setState({ - isFlyoutVisible: false, - info: null, // force re-read for next click - }); - }; - - private showFlyout = () => { - this.setState({ isFlyoutVisible: true }); - - if (!this.state.info) { - this.loadInfo(); - } - }; -} - -export const ReportInfoButton = injectI18n(ReportInfoButtonUi); diff --git a/x-pack/plugins/reporting/public/management/report_listing.scss b/x-pack/plugins/reporting/public/management/report_listing.scss new file mode 100644 index 00000000000000..7e85838ba09cda --- /dev/null +++ b/x-pack/plugins/reporting/public/management/report_listing.scss @@ -0,0 +1,7 @@ +.kbnReporting { + &__reportListing { + &__typeIcon { + padding-left: $euiSizeS; + } + } +} diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index 5e80c2d666c23b..577d64be38a546 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -7,16 +7,17 @@ import { registerTestBed } from '@kbn/test/jest'; import type { SerializableRecord, UnwrapPromise } from '@kbn/utility-types'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import React from 'react'; import { act } from 'react-dom/test-utils'; import type { Observable } from 'rxjs'; +import type { IUiSettingsClient } from 'src/core/public'; import { ListingProps as Props, ReportListing } from '.'; import type { NotificationsSetup } from '../../../../../src/core/public'; import { applicationServiceMock, httpServiceMock, notificationServiceMock, + coreMock, } from '../../../../../src/core/public/mocks'; import type { LocatorPublic, SharePluginSetup } from '../../../../../src/plugins/share/public'; import type { ILicense } from '../../../licensing/public'; @@ -65,12 +66,18 @@ const mockJobs: ReportApiJSON[] = [ id: 'k90e51pk1ieucbae0c3t8wo2', attempts: 0, created_at: '2020-04-14T21:01:13.064Z', - jobtype: 'printable_pdf', + jobtype: 'printable_pdf_v2', meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { + spaceId: 'my-space', objectType: 'canvas workpad', title: 'My Canvas Workpad', - }, + locatorParams: [ + { + id: 'MY_APP', + }, + ], + } as any, status: 'pending', }), buildMockReport({ @@ -201,12 +208,6 @@ const mockJobs: ReportApiJSON[] = [ }), ]; -const reportingAPIClient = { - list: jest.fn(() => Promise.resolve(mockJobs.map((j) => new Job(j)))), - total: jest.fn(() => Promise.resolve(18)), - migrateReportingIndicesIlmPolicy: jest.fn(), -} as unknown as DeeplyMockedKeys; - const validCheck = { check: () => ({ state: 'VALID', @@ -233,11 +234,13 @@ const mockPollConfig = { describe('ReportListing', () => { let httpService: ReturnType; + let uiSettingsClient: IUiSettingsClient; let applicationService: ReturnType; let ilmLocator: undefined | LocatorPublic; let urlService: SharePluginSetup['url']; let testBed: UnwrapPromise>; let toasts: NotificationsSetup['toasts']; + let reportingAPIClient: ReportingAPIClient; const createTestBed = registerTestBed( (props?: Partial) => ( @@ -290,6 +293,7 @@ describe('ReportListing', () => { beforeEach(async () => { toasts = notificationServiceMock.createSetupContract().toasts; httpService = httpServiceMock.createSetupContract(); + uiSettingsClient = coreMock.createSetup().uiSettings; applicationService = applicationServiceMock.createStartContract(); applicationService.capabilities = { catalogue: {}, @@ -300,6 +304,16 @@ describe('ReportListing', () => { getUrl: jest.fn(), } as unknown as LocatorPublic; + reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); + + jest + .spyOn(reportingAPIClient, 'list') + .mockImplementation(() => Promise.resolve(mockJobs.map((j) => new Job(j)))); + jest.spyOn(reportingAPIClient, 'total').mockImplementation(() => Promise.resolve(18)); + jest + .spyOn(reportingAPIClient, 'migrateReportingIndicesIlmPolicy') + .mockImplementation(jest.fn()); + urlService = { locators: { get: () => ilmLocator, @@ -312,10 +326,9 @@ describe('ReportListing', () => { jest.clearAllMocks(); }); - it('Report job listing with some items', () => { - const { actions } = testBed; - const table = actions.findListTable(); - expect(table).toMatchSnapshot(); + it('renders a listing with some items', () => { + const { find } = testBed; + expect(find('reportDownloadLink').length).toBe(mockJobs.length); }); it('subscribes to license changes, and unsubscribes on dismount', async () => { @@ -334,6 +347,21 @@ describe('ReportListing', () => { expect(unsubscribeMock).toHaveBeenCalled(); }); + it('navigates to a Kibana App in a new tab and is spaces aware', () => { + const { find } = testBed; + + jest.spyOn(window, 'open').mockImplementation(jest.fn()); + jest.spyOn(window, 'focus').mockImplementation(jest.fn()); + + find('euiCollapsedItemActionsButton').first().simulate('click'); + find('reportOpenInKibanaApp').first().simulate('click'); + + expect(window.open).toHaveBeenCalledWith( + '/s/my-space/app/reportingRedirect?jobId=k90e51pk1ieucbae0c3t8wo2', + '_blank' + ); + }); + describe('ILM policy', () => { beforeEach(async () => { httpService = httpServiceMock.createSetupContract(); @@ -414,7 +442,9 @@ describe('ReportListing', () => { it('informs users when migrations failed', async () => { const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; httpService.get.mockResolvedValueOnce({ status }); - reportingAPIClient.migrateReportingIndicesIlmPolicy.mockRejectedValueOnce(new Error('oops!')); + (reportingAPIClient.migrateReportingIndicesIlmPolicy as jest.Mock).mockRejectedValueOnce( + new Error('oops!') + ); await runSetup(); const { actions } = testBed; diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 6b46778011250a..46c375cd8880fa 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -12,15 +12,17 @@ import { EuiLoadingSpinner, EuiPageHeader, EuiSpacer, - EuiText, - EuiTextColor, + EuiBasicTableColumn, + EuiIconTip, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ILicense } from '../../../licensing/public'; -import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../common/constants'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID, JOB_STATUSES } from '../../common/constants'; +import { prettyPrintJobType } from '../../common/job_utils'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; import { useIlmPolicyStatus } from '../lib/ilm_policy_status_context'; @@ -28,13 +30,20 @@ import { Job } from '../lib/job'; import { checkLicense } from '../lib/license_check'; import { useInternalApiClient } from '../lib/reporting_api_client'; import { useKibana } from '../shared_imports'; -import { IlmPolicyLink } from './ilm_policy_link'; -import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; -import { ReportDeleteButton } from './report_delete_button'; -import { ReportDiagnostic } from './report_diagnostic'; -import { ReportDownloadButton } from './report_download_button'; -import { ReportInfoButton } from './report_info_button'; import { ListingProps as Props } from './'; +import { PDF_JOB_TYPE_V2, PNG_JOB_TYPE_V2 } from '../../common/constants'; +import { + IlmPolicyLink, + MigrateIlmPolicyCallOut, + ReportDeleteButton, + ReportDiagnostic, + ReportStatusIndicator, + ReportInfoFlyout, +} from './components'; +import { guessAppIconTypeFromObjectType } from './utils'; +import './report_listing.scss'; + +type TableColumn = EuiBasicTableColumn; interface State { page: number; @@ -45,6 +54,7 @@ interface State { showLinks: boolean; enableLinks: boolean; badLicenseMessage: string; + selectedJob: undefined | Job; } class ReportListingUi extends Component { @@ -65,6 +75,7 @@ class ReportListingUi extends Component { showLinks: false, enableLinks: false, badLicenseMessage: '', + selectedJob: undefined, }; this.isInitialJobsFetch = true; @@ -177,23 +188,19 @@ class ReportListingUi extends Component { await this.props.apiClient.deleteReport(job.id); this.removeJob(job); this.props.toasts.addSuccess( - this.props.intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteConfim', - defaultMessage: `The {reportTitle} report was deleted`, + i18n.translate('xpack.reporting.listing.table.deleteConfim', { + defaultMessage: `The {reportTitle} report was deleted`, + values: { + reportTitle: job.title, }, - { reportTitle: job.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 } - ) + i18n.translate('xpack.reporting.listing.table.deleteFailedErrorMessage', { + defaultMessage: `The report was not deleted: {error}`, + values: { error }, + }) ); throw error; } @@ -240,8 +247,7 @@ class ReportListingUi extends Component { if (fetchError.message === 'Failed to fetch') { this.props.toasts.addDanger( fetchError.message || - this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.requestFailedErrorMessage', + i18n.translate('xpack.reporting.listing.table.requestFailedErrorMessage', { defaultMessage: 'Request failed', }) ); @@ -265,61 +271,176 @@ class ReportListingUi extends Component { return this.state.showLinks && this.state.enableLinks; }; - private renderTable() { - const { intl } = this.props; + /** + * Widths like this are not the best, but the auto-layout does not play well with text in links. We can update + * this with something that works better on all screen sizes. This works for desktop, mobile fallback is provided on a + * per column basis. + */ + private readonly tableColumnWidths = { + type: '5%', + title: '30%', + status: '20%', + createdAt: '25%', + content: '10%', + actions: '10%', + }; - const tableColumns = [ + private renderTable() { + const { tableColumnWidths } = this; + const tableColumns: TableColumn[] = [ + { + field: 'type', + width: tableColumnWidths.type, + name: i18n.translate('xpack.reporting.listing.tableColumns.typeTitle', { + defaultMessage: 'Type', + }), + render: (_type: string, job) => { + return ( +
+ +
+ ); + }, + mobileOptions: { + show: true, + render: (job) => { + return
{job.objectType}
; + }, + }, + }, { field: 'title', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.reportTitle', - defaultMessage: 'Report', + name: i18n.translate('xpack.reporting.listing.tableColumns.reportTitle', { + defaultMessage: 'Title', }), - render: (objectTitle: string, job: Job) => { + width: tableColumnWidths.title, + render: (objectTitle: string, job) => { return (
-
{objectTitle}
- - {job.objectType} - + this.setState({ selectedJob: job })}> + {objectTitle || + i18n.translate('xpack.reporting.listing.table.noTitleLabel', { + defaultMessage: 'Untitled', + })} +
); }, + mobileOptions: { + header: false, + width: '100%', // This is not recognized by EUI types but has an effect, leaving for now + } as unknown as { header: boolean }, + }, + { + field: 'status', + width: tableColumnWidths.status, + name: i18n.translate('xpack.reporting.listing.tableColumns.statusTitle', { + defaultMessage: 'Status', + }), + render: (_status: string, job) => { + return ( + + + + ); + }, + mobileOptions: { + show: false, + }, }, { field: 'created_at', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.createdAtTitle', + width: tableColumnWidths.createdAt, + name: i18n.translate('xpack.reporting.listing.tableColumns.createdAtTitle', { defaultMessage: 'Created at', }), - render: (_createdAt: string, job: Job) => ( -
{job.getCreatedAtLabel()}
+ render: (_createdAt: string, job) => ( +
{job.getCreatedAtDate()}
), + mobileOptions: { + show: false, + }, }, { - field: 'status', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.statusTitle', - defaultMessage: 'Status', + field: 'content', + width: tableColumnWidths.content, + name: i18n.translate('xpack.reporting.listing.tableColumns.content', { + defaultMessage: 'Content', }), - render: (_status: string, job: Job) => ( -
{job.getStatusLabel()}
- ), + render: (_status: string, job) => prettyPrintJobType(job.jobtype), + mobileOptions: { + show: false, + }, }, { - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.actionsTitle', + name: i18n.translate('xpack.reporting.listing.tableColumns.actionsTitle', { defaultMessage: 'Actions', }), + width: tableColumnWidths.actions, actions: [ { - render: (job: Job) => { - return ( -
- - -
- ); + isPrimary: true, + 'data-test-subj': 'reportDownloadLink', + type: 'icon', + icon: 'download', + name: i18n.translate('xpack.reporting.listing.table.downloadReportButtonLabel', { + defaultMessage: 'Download report', + }), + description: i18n.translate('xpack.reporting.listing.table.downloadReportDescription', { + defaultMessage: 'Download this report in a new tab.', + }), + onClick: (job) => this.props.apiClient.downloadReport(job.id), + enabled: (job) => + job.status === JOB_STATUSES.COMPLETED || job.status === JOB_STATUSES.WARNINGS, + }, + { + name: i18n.translate( + 'xpack.reporting.listing.table.viewReportingInfoActionButtonLabel', + { + defaultMessage: 'View report info', + } + ), + description: i18n.translate( + 'xpack.reporting.listing.table.viewReportingInfoActionButtonDescription', + { + defaultMessage: 'View additional information about this report.', + } + ), + type: 'icon', + icon: 'iInCircle', + onClick: (job) => this.setState({ selectedJob: job }), + }, + { + name: i18n.translate('xpack.reporting.listing.table.openInKibanaAppLabel', { + defaultMessage: 'Open in Kibana App', + }), + 'data-test-subj': 'reportOpenInKibanaApp', + description: i18n.translate( + 'xpack.reporting.listing.table.openInKibanaAppDescription', + { + defaultMessage: 'Open the Kibana App where this report was generated.', + } + ), + available: (job) => + [PDF_JOB_TYPE_V2, PNG_JOB_TYPE_V2].some( + (linkableJobType) => linkableJobType === job.jobtype + ), + type: 'icon', + icon: 'popout', + onClick: (job) => { + const href = this.props.apiClient.getKibanaAppHref(job); + window.open(href, '_blank'); + window.focus(); }, }, ], @@ -358,12 +479,10 @@ class ReportListingUi extends Component { columns={tableColumns} noItemsMessage={ this.state.isLoading - ? intl.formatMessage({ - id: 'xpack.reporting.listing.table.loadingReportsDescription', + ? i18n.translate('xpack.reporting.listing.table.loadingReportsDescription', { defaultMessage: 'Loading reports', }) - : intl.formatMessage({ - id: 'xpack.reporting.listing.table.noCreatedReportsDescription', + : i18n.translate('xpack.reporting.listing.table.noCreatedReportsDescription', { defaultMessage: 'No reports have been created', }) } @@ -374,13 +493,17 @@ class ReportListingUi extends Component { data-test-subj={REPORT_TABLE_ID} rowProps={() => ({ 'data-test-subj': REPORT_TABLE_ROW_ID })} /> + {!!this.state.selectedJob && ( + this.setState({ selectedJob: undefined })} + job={this.state.selectedJob} + /> + )} ); } } -const PrivateReportListing = injectI18n(ReportListingUi); - export const ReportListing = ( props: Omit ) => { @@ -392,7 +515,7 @@ export const ReportListing = ( }, } = useKibana(); return ( - { + switch (type) { + case 'search': + return 'discoverApp'; + case 'dashboard': + return 'dashboardApp'; + case 'visualization': + return 'visualizeApp'; + case 'canvas workpad': + return 'canvasApp'; + default: + return 'apps'; + } +}; + +export const jobHasIssues = (job: Job): boolean => { + return ( + Boolean(job.getWarnings()) || + [JOB_STATUSES.WARNINGS, JOB_STATUSES.FAILED].some((status) => job.status === status) + ); +}; diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 7fd6047470a0e9..fe80ed679c8ed6 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -41,9 +41,9 @@ import type { UiActionsSetup, UiActionsStart, } from './shared_imports'; +import { AppNavLinkStatus } from './shared_imports'; import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; -import { isRedirectAppPath } from './utils'; export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; @@ -173,15 +173,6 @@ export class ReportingPublicPlugin title: this.title, order: 1, mount: async (params) => { - // The redirect app will be mounted if reporting is opened on a specific path. The redirect app expects a - // specific environment to be present so that it can navigate to a specific application. This is used by - // report generation to navigate to the correct place with full app state. - if (isRedirectAppPath(params.history.location.pathname)) { - const { mountRedirectApp } = await import('./redirect'); - return mountRedirectApp({ ...params, share, apiClient }); - } - - // Otherwise load the reporting management UI. params.setBreadcrumbs([{ text: this.breadcrumbText }]); const [[start], { mountManagementSection }] = await Promise.all([ getStartServices(), @@ -208,6 +199,19 @@ export class ReportingPublicPlugin }, }); + core.application.register({ + id: 'reportingRedirect', + mount: async (params) => { + const { mountRedirectApp } = await import('./redirect'); + return mountRedirectApp({ ...params, share, apiClient }); + }, + title: 'Reporting redirect app', + searchable: false, + chromeless: true, + exactRoute: true, + navLinkStatus: AppNavLinkStatus.hidden, + }); + uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, new ReportingCsvPanelAction({ core, apiClient, startServices$, license$, usesUiCapabilities }) diff --git a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx index 4bf6d40acb1704..eb34fc71cbf4e1 100644 --- a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx @@ -9,12 +9,13 @@ import { render, unmountComponentAtNode } from 'react-dom'; import React from 'react'; import { EuiErrorBoundary } from '@elastic/eui'; -import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; +import type { AppMountParameters } from 'kibana/public'; +import type { SharePluginSetup } from '../shared_imports'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; import { RedirectApp } from './redirect_app'; -interface MountParams extends ManagementAppMountParams { +interface MountParams extends AppMountParameters { apiClient: ReportingAPIClient; share: SharePluginSetup; } diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.scss b/x-pack/plugins/reporting/public/redirect/redirect_app.scss new file mode 100644 index 00000000000000..7e6b40e8231e8a --- /dev/null +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.scss @@ -0,0 +1,8 @@ +.reportingRedirectApp { + &__interstitialPage { + /* + Create some padding above and below the page so that the errors (if any) display nicely. + */ + margin: $euiSizeXXL auto; + } +} diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx index cf027e2a461965..4b271b17c5e857 100644 --- a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx @@ -7,8 +7,9 @@ import React, { useEffect, useState } from 'react'; import type { FunctionComponent } from 'react'; +import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; -import { EuiTitle, EuiCallOut, EuiCodeBlock } from '@elastic/eui'; +import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; import type { ScopedHistory } from 'src/core/public'; @@ -18,6 +19,8 @@ import { LocatorParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import type { SharePluginSetup } from '../shared_imports'; +import './redirect_app.scss'; + interface Props { apiClient: ReportingAPIClient; history: ScopedHistory; @@ -28,9 +31,6 @@ const i18nTexts = { errorTitle: i18n.translate('xpack.reporting.redirectApp.errorTitle', { defaultMessage: 'Redirect error', }), - redirectingTitle: i18n.translate('xpack.reporting.redirectApp.redirectingMessage', { - defaultMessage: 'Redirecting...', - }), consoleMessagePrefix: i18n.translate( 'xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel', { @@ -39,36 +39,51 @@ const i18nTexts = { ), }; -export const RedirectApp: FunctionComponent = ({ share }) => { +export const RedirectApp: FunctionComponent = ({ share, apiClient }) => { const [error, setError] = useState(); useEffect(() => { - try { - const locatorParams = (window as unknown as Record)[ - REPORTING_REDIRECT_LOCATOR_STORE_KEY - ]; + (async () => { + try { + let locatorParams: undefined | LocatorParams; - if (!locatorParams) { - throw new Error('Could not find locator params for report'); - } + const { jobId } = parse(window.location.search); - share.navigate(locatorParams); - } catch (e) { - setError(e); - // eslint-disable-next-line no-console - console.error(i18nTexts.consoleMessagePrefix, e.message); - throw e; - } - }, [share]); + if (jobId) { + const result = await apiClient.getInfo(jobId as string); + locatorParams = result?.locatorParams?.[0]; + } else { + locatorParams = (window as unknown as Record)[ + REPORTING_REDIRECT_LOCATOR_STORE_KEY + ]; + } + + if (!locatorParams) { + throw new Error('Could not find locator params for report'); + } + + share.navigate(locatorParams); + } catch (e) { + setError(e); + // eslint-disable-next-line no-console + console.error(i18nTexts.consoleMessagePrefix, e.message); + throw e; + } + })(); + }, [share, apiClient]); - return error ? ( - -

{error.message}

- {error.stack && {error.stack}} -
- ) : ( - -

{i18nTexts.redirectingTitle}

-
+ return ( +
+ {error ? ( + +

{error.message}

+ {error.stack && {error.stack}} +
+ ) : ( + // We don't show anything on this page, the share service will handle showing any issues with + // using the locator +
+ )} +
); }; diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts index 30e6cd12e3ed91..037dc5e374d25b 100644 --- a/x-pack/plugins/reporting/public/shared_imports.ts +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -7,6 +7,8 @@ export type { SharePluginSetup, SharePluginStart, LocatorPublic } from 'src/plugins/share/public'; +export { AppNavLinkStatus } from '../../../../src/core/public'; + export type { UseRequestResponse } from '../../../../src/plugins/es_ui_shared/public'; export { useRequest } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/reporting/public/utils.ts b/x-pack/plugins/reporting/public/utils.ts deleted file mode 100644 index f39c7ef2174ef6..00000000000000 --- a/x-pack/plugins/reporting/public/utils.ts +++ /dev/null @@ -1,12 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { REACT_ROUTER_REDIRECT_APP_PATH } from './constants'; - -export const isRedirectAppPath = (pathname: string) => { - return pathname.startsWith(REACT_ROUTER_REDIRECT_APP_PATH); -}; diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts index 7a2ec5b83e7f45..69baf143156fd6 100644 --- a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts @@ -33,13 +33,13 @@ describe('getFullRedirectAppUrl', () => { test('smoke test', () => { expect(getFullRedirectAppUrl(config, 'test', undefined)).toBe( - 'http://localhost:1234/test/s/test/app/management/insightsAndAlerting/reporting/r' + 'http://localhost:1234/test/s/test/app/reportingRedirect' ); }); test('adding forceNow', () => { expect(getFullRedirectAppUrl(config, 'test', 'TEST with a space')).toBe( - 'http://localhost:1234/test/s/test/app/management/insightsAndAlerting/reporting/r?forceNow=TEST%20with%20a%20space' + 'http://localhost:1234/test/s/test/app/reportingRedirect?forceNow=TEST%20with%20a%20space' ); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index 3cf3c057e7b9c2..ba076f98996b18 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -102,7 +102,7 @@ test(`passes browserTimezone to generatePng`, async () => { "warning": [Function], }, Array [ - "localhost:80undefined/app/management/insightsAndAlerting/reporting/r?forceNow=test", + "localhost:80undefined/app/reportingRedirect?forceNow=test", Object { "id": "test", "params": Object {}, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d450f679906bd8..8022cdcacfff72 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19141,9 +19141,6 @@ "xpack.reporting.listing.table.deleteConfirmTitle": "「{name}」レポートを削除しますか?", "xpack.reporting.listing.table.deleteFailedErrorMessage": "レポートは削除されませんでした:{error}", "xpack.reporting.listing.table.deleteNumConfirmTitle": "{num} 件のレポートを削除しますか?", - "xpack.reporting.listing.table.downloadReport": "レポートをダウンロード", - "xpack.reporting.listing.table.downloadReportAriaLabel": "レポートをダウンロード", - "xpack.reporting.listing.table.downloadReportWithWarnings": "警告があるレポートをダウンロード", "xpack.reporting.listing.table.loadingReportsDescription": "レポートを読み込み中です", "xpack.reporting.listing.table.noCreatedReportsDescription": "レポートが作成されていません", "xpack.reporting.listing.table.reportCalloutExportTypeDeprecated": "{jobtype}(廃止予定)", @@ -19187,7 +19184,6 @@ "xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "{reportObjectType}「{reportObjectTitle}」のレポートが作成されました", "xpack.reporting.redirectApp.errorTitle": "リダイレクトエラー", "xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel": "リダイレクトページエラー:", - "xpack.reporting.redirectApp.redirectingMessage": "リダイレクト中...", "xpack.reporting.registerFeature.reportingDescription": "Discover、可視化、ダッシュボードから生成されたレポートを管理します。", "xpack.reporting.registerFeature.reportingTitle": "レポート", "xpack.reporting.screencapture.browserWasClosed": "ブラウザーは予期せず終了しました。詳細については、サーバーログを確認してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8839c6bab9acaa..e753eda0399aeb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19418,9 +19418,6 @@ "xpack.reporting.listing.table.deleteFailedErrorMessage": "报告未删除:{error}", "xpack.reporting.listing.table.deleteNumConfirmTitle": "删除 {num} 个报告?", "xpack.reporting.listing.table.deleteReportButton": "删除{num, plural, other {报告} }", - "xpack.reporting.listing.table.downloadReport": "下载报告", - "xpack.reporting.listing.table.downloadReportAriaLabel": "下载报告", - "xpack.reporting.listing.table.downloadReportWithWarnings": "下载包含警告的报告", "xpack.reporting.listing.table.loadingReportsDescription": "正在载入报告", "xpack.reporting.listing.table.noCreatedReportsDescription": "未创建任何报告", "xpack.reporting.listing.table.reportCalloutExportTypeDeprecated": "{jobtype}(已弃用)", @@ -19464,7 +19461,6 @@ "xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "已为 {reportObjectType}“{reportObjectTitle}”创建报告", "xpack.reporting.redirectApp.errorTitle": "重定向错误", "xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel": "重定向页面错误:", - "xpack.reporting.redirectApp.redirectingMessage": "正在重定向......", "xpack.reporting.registerFeature.reportingDescription": "管理您从 Discover、Visualize 和 Dashboard 生成的报告。", "xpack.reporting.registerFeature.reportingTitle": "Reporting", "xpack.reporting.screencapture.browserWasClosed": "浏览器已意外关闭!有关更多信息,请查看服务器日志。", diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 1291fb686d8b14..54320e431d33b2 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -83,63 +83,63 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { Array [ Object { "actions": "", - "createdAt": "2021-07-19 @ 10:29 PMtest_user", - "report": "Automated reportsearch", - "status": "Completed at 2021-07-19 @ 10:29 PM See report info for warnings. This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.", + "createdAt": "2021-07-19 @ 10:29 PM", + "report": "Automated report", + "status": "Done, warnings detected", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:47 PMtest_user", - "report": "Discover search [2021-07-19T11:47:35.995-07:00]search", - "status": "Completed at 2021-07-19 @ 06:47 PM", + "createdAt": "2021-07-19 @ 06:47 PM", + "report": "Discover search [2021-07-19T11:47:35.995-07:00]", + "status": "Done", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:46 PMtest_user", - "report": "Discover search [2021-07-19T11:46:00.132-07:00]search", - "status": "Completed at 2021-07-19 @ 06:46 PM See report info for warnings.", + "createdAt": "2021-07-19 @ 06:46 PM", + "report": "Discover search [2021-07-19T11:46:00.132-07:00]", + "status": "Done, warnings detected", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:44 PMtest_user", - "report": "Discover search [2021-07-19T11:44:48.670-07:00]search", - "status": "Completed at 2021-07-19 @ 06:44 PM See report info for warnings.", + "createdAt": "2021-07-19 @ 06:44 PM", + "report": "Discover search [2021-07-19T11:44:48.670-07:00]", + "status": "Done, warnings detected", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:41 PMtest_user", - "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Pending at 2021-07-19 @ 06:41 PM Waiting for job to process.", + "createdAt": "2021-07-19 @ 06:41 PM", + "report": "[Flights] Global Flight Dashboard", + "status": "Pending", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:41 PMtest_user", - "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Failed at 2021-07-19 @ 06:43 PM See report info for error details.", + "createdAt": "2021-07-19 @ 06:41 PM", + "report": "[Flights] Global Flight Dashboard", + "status": "Failed", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:41 PMtest_user", - "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Completed at 2021-07-19 @ 06:41 PM See report info for warnings.", + "createdAt": "2021-07-19 @ 06:41 PM", + "report": "[Flights] Global Flight Dashboard", + "status": "Done, warnings detected", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:38 PMtest_user", - "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Completed at 2021-07-19 @ 06:39 PM See report info for warnings.", + "createdAt": "2021-07-19 @ 06:38 PM", + "report": "[Flights] Global Flight Dashboard", + "status": "Done, warnings detected", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:38 PMtest_user", - "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Completed at 2021-07-19 @ 06:39 PM", + "createdAt": "2021-07-19 @ 06:38 PM", + "report": "[Flights] Global Flight Dashboard", + "status": "Done", }, Object { "actions": "", - "createdAt": "2021-07-19 @ 06:38 PMtest_user", - "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Completed at 2021-07-19 @ 06:38 PM", + "createdAt": "2021-07-19 @ 06:38 PM", + "report": "[Flights] Global Flight Dashboard", + "status": "Done", }, ] `); diff --git a/x-pack/test/reporting_functional/reporting_and_security/management.ts b/x-pack/test/reporting_functional/reporting_and_security/management.ts index 304c175f0cb5dd..4af6dbdac1a655 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/management.ts @@ -9,8 +9,9 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService, getPageObjects }: FtrProviderContext) => { - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'reporting', 'dashboard']); const testSubjects = getService('testSubjects'); + const browser = getService('browser'); const reportingFunctional = getService('reportingFunctional'); describe('Access to Management > Reporting', () => { @@ -32,5 +33,26 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await PageObjects.common.navigateToApp('reporting'); await testSubjects.existOrFail('reportJobListing'); }); + + it('Allows users to navigate back to where a report was generated', async () => { + const dashboardTitle = 'Ecom Dashboard'; + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard(dashboardTitle); + + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + await PageObjects.common.navigateToApp('reporting'); + await PageObjects.common.sleep(3000); // Wait an amount of time for auto-polling to refresh the jobs + + // We do not need to wait for the report to finish generating + await (await testSubjects.find('euiCollapsedItemActionsButton')).click(); + await (await testSubjects.find('reportOpenInKibanaApp')).click(); + + const [, dashboardWindowHandle] = await browser.getAllWindowHandles(); + await browser.switchToWindow(dashboardWindowHandle); + + await PageObjects.dashboard.expectOnDashboard(dashboardTitle); + }); }); }; diff --git a/x-pack/test/reporting_functional/reporting_without_security/management.ts b/x-pack/test/reporting_functional/reporting_without_security/management.ts index a97cb211b7c0e9..b0088ccb9a9055 100644 --- a/x-pack/test/reporting_functional/reporting_without_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_without_security/management.ts @@ -53,10 +53,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await PageObjects.common.sleep(3000); // Wait an amount of time for auto-polling to refresh the jobs - const tableElem = await testSubjects.find('reportJobListing'); - const tableRow = await tableElem.findByCssSelector('tbody tr td+td'); // find the title cell of the first row - const tableCellText = await tableRow.getVisibleText(); - expect(tableCellText).to.be(`Tiểu thuyết\nvisualization`); + const [firstTitleElem] = await testSubjects.findAll('reportingListItemObjectTitle'); + const tableCellText = await firstTitleElem.getVisibleText(); + expect(tableCellText).to.be(`Tiểu thuyết`); }); }); };