From 1e6bc1709e9639fd8ab4977d21ec511b1d62238c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 6 Jun 2025 13:03:47 +0100 Subject: [PATCH 01/15] initial commit --- .../private/reporting/public/constants.ts | 15 ++ .../management/components/reporting_tabs.tsx | 140 ++++++++++++++++++ .../default/report_listing_default.tsx | 50 ------- .../management/mount_management_section.tsx | 61 ++++++-- ...ing_table.tsx => report_exports_table.tsx} | 5 +- .../public/management/report_listing.tsx | 58 +++++++- .../stateful/report_listing_stateful.tsx | 88 ----------- .../private/reporting/public/plugin.ts | 14 +- .../private/reporting/public/translations.ts | 16 ++ 9 files changed, 280 insertions(+), 167 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/constants.ts create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx delete mode 100644 x-pack/platform/plugins/private/reporting/public/management/default/report_listing_default.tsx rename x-pack/platform/plugins/private/reporting/public/management/{report_listing_table.tsx => report_exports_table.tsx} (98%) delete mode 100644 x-pack/platform/plugins/private/reporting/public/management/stateful/report_listing_stateful.tsx create mode 100644 x-pack/platform/plugins/private/reporting/public/translations.ts diff --git a/x-pack/platform/plugins/private/reporting/public/constants.ts b/x-pack/platform/plugins/private/reporting/public/constants.ts new file mode 100644 index 0000000000000..193e9cf234af3 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/constants.ts @@ -0,0 +1,15 @@ +/* + * 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 APP_PATH = '/app/management/insightsAndAlerting/reporting' as const; +export const HOME_PATH = `/`; +export const REPORTING_EXPORTS_PATH = '/exports' as const; +export const REPORTING_SCHEDULES_PATH = '/schedules' as const; +export const EXPORTS_TAB_ID = 'exports' as const; +export const SCHEDULES_TAB_ID = 'schedules' as const; + +export type Section = 'exports' | 'schedules'; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx new file mode 100644 index 0000000000000..8c3cca2464164 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -0,0 +1,140 @@ +/* + * 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, { lazy, useCallback, useEffect } from 'react'; +import { EuiPageHeader, EuiPageTemplate, EuiTab, EuiTabs } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { REPORTING_EXPORTS_PATH, REPORTING_SCHEDULES_PATH, Section } from '../../constants'; +import { Route, Routes } from '@kbn/shared-ux-router'; +import { suspendedComponentWithProps } from '../../suspended_component_with_props'; +import { RouteComponentProps } from 'react-router'; +import { CoreStart, ScopedHistory } from '@kbn/core/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { ClientConfigType, ReportingAPIClient, useKibana } from '@kbn/reporting-public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; +import { getReportingSectionBreadcrumb } from './get_reporting_sections_breadcrumb'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const ReportListing = lazy(() => import('../report_listing')); + +export interface MatchParams { + section: Section; +} + +export interface ReportingTabsProps { + coreStart: CoreStart; + license$: LicensingPluginStart['license$']; + dataService: DataPublicPluginStart; + shareService: SharePluginStart; + config: ClientConfigType; + apiClient: ReportingAPIClient; +} + +export const ReportingTabs: React.FunctionComponent< + Partial & ReportingTabsProps +> = (props) => { + const { coreStart, license$, shareService, config, apiClient, ...rest } = props; + const { application, notifications } = coreStart; + const { section } = rest.match?.params as MatchParams; + const history = rest.history as ScopedHistory; + const tabs = [ + { + id: 'exports', + name: i18n.translate('xpack.reporting.tabs.exports', { + defaultMessage: 'Exports', + }), + }, + { + id: 'schedules', + name: i18n.translate('xpack.reporting.tabs.schedules', { + defaultMessage: 'Schedules', + }), + }, + ]; + + const renderExportsList = useCallback(() => { + return suspendedComponentWithProps( + ReportListing, + + 'xl' + )({ + apiClient, + toasts: notifications.toasts, + license$, + config, + redirect: application.navigateToApp, + navigateToUrl: application.navigateToUrl, + urlService: shareService.url, + }); + }, []); + + const renderSchedulesList = useCallback(() => { + return ( + + {suspendedComponentWithProps( + ReportListing, + 'xl' + )({ + apiClient, + toasts: notifications.toasts, + license$, + config, + redirect: application.navigateToApp, + navigateToUrl: application.navigateToUrl, + urlService: shareService.url, + })} + + ); + }, []); + + const onSectionChange = (newSection: Section) => { + history.push(`/${newSection}`); + }; + + return ( + <> + + } + description={ + + } + /> + + + {tabs.map(({ id, name }) => ( + onSectionChange(id as Section)} + isSelected={section === id} + data-test-subj={`connectionDetailsTabBtn-${id}`} + > + {name} + + ))} + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ReportingTabs as default }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/default/report_listing_default.tsx b/x-pack/platform/plugins/private/reporting/public/management/default/report_listing_default.tsx deleted file mode 100644 index a2b09ccd3c177..0000000000000 --- a/x-pack/platform/plugins/private/reporting/public/management/default/report_listing_default.tsx +++ /dev/null @@ -1,50 +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, { FC } from 'react'; - -import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { ListingPropsInternal } from '..'; -import { ReportListingTable } from '../report_listing_table'; - -/** - * Used in non-stateful (Serverless) - * Does not render controls for features only applicable in Stateful - */ -export const ReportListingDefault: FC = (props) => { - const { apiClient, capabilities, config, navigateToUrl, toasts, urlService, ...listingProps } = - props; - return ( - <> - - } - description={ - - } - /> - - - - ); -}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx b/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx index 986a34d3c55b6..e59322a48224d 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import * as React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; +import React, { Suspense, lazy } from 'react'; +import ReactDOM from 'react-dom'; import type { CoreStart, NotificationsStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -24,8 +24,13 @@ import { import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '../query_client'; -import { ReportListing } from '.'; import { PolicyStatusContextProvider } from '../lib/default_status_context'; +import { Section } from '../constants'; +import { Route, Router, Routes } from '@kbn/shared-ux-router'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { Redirect } from 'react-router'; + +const ReportingTabs = lazy(() => import('./components/reporting_tabs')); export async function mountManagementSection({ coreStart, @@ -59,31 +64,57 @@ export async function mountManagementSection({ actions: actionsService, notifications: notificationsService, }; + const sections: Section[] = ['exports', 'schedules']; + const { element, history } = params; + + const sectionsRegex = sections.join('|'); - render( + ReactDOM.render( - + + + { + console.log('Redirecting to Reporting Home', { + routerProps: params, + history, + coreStart, + config, + apiClient, + license$, + }); + return ( + }> + + + ); + }} + /> + + + , - params.element + element ); return () => { - unmountComponentAtNode(params.element); + ReactDOM.unmountComponentAtNode(element); }; } diff --git a/x-pack/platform/plugins/private/reporting/public/management/report_listing_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/report_exports_table.tsx similarity index 98% rename from x-pack/platform/plugins/private/reporting/public/management/report_listing_table.tsx rename to x-pack/platform/plugins/private/reporting/public/management/report_exports_table.tsx index 848c25a4ba278..687c5943c5ed5 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/report_listing_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/report_exports_table.tsx @@ -44,7 +44,7 @@ interface State { selectedJob: undefined | Job; } -export class ReportListingTable extends Component { +export class ReportExportsTable extends Component { private isInitialJobsFetch: boolean; private licenseSubscription?: Subscription; private mounted?: boolean; @@ -438,3 +438,6 @@ export class ReportListingTable extends Component { ); } } + +// eslint-disable-next-line import/no-default-export +export { ReportExportsTable as default }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx b/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx index a658d2e190051..b25d926a32b60 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx @@ -7,20 +7,66 @@ import React from 'react'; import { useInternalApiClient, useKibana } from '@kbn/reporting-public'; -import { ReportListingStateful } from './stateful/report_listing_stateful'; -import { ReportListingDefault } from './default/report_listing_default'; import { ListingProps } from '.'; +import { IlmPolicyLink, MigrateIlmPolicyCallOut, ReportDiagnostic } from './components'; +import { ReportExportsTable } from './report_exports_table'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { useIlmPolicyStatus } from '../lib/ilm_policy_status_context'; export const ReportListing = (props: ListingProps) => { + console.log('Inside report listing', { props }); const { apiClient } = useInternalApiClient(); const { services: { application: { capabilities }, }, } = useKibana(); - return props.config.statefulSettings.enabled ? ( - - ) : ( - + + const { config, navigateToUrl, toasts, urlService, ...listingProps } = props; + const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); + const ilmPolicyContextValue = useIlmPolicyStatus(); + const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; + const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); + return ( + <> + {props.config.statefulSettings.enabled ? : null} + + + + + + {props.config.statefulSettings.enabled ? ( + <> + + + {capabilities?.management?.data?.index_lifecycle_management && ( + + {ilmPolicyContextValue?.isLoading ? ( + + ) : ( + showIlmPolicyLink && ( + + ) + )} + + )} + + + + + + ) : null} + ); }; + +// eslint-disable-next-line import/no-default-export +export { ReportListing as default }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/stateful/report_listing_stateful.tsx b/x-pack/platform/plugins/private/reporting/public/management/stateful/report_listing_stateful.tsx deleted file mode 100644 index 910a32f7a5aed..0000000000000 --- a/x-pack/platform/plugins/private/reporting/public/management/stateful/report_listing_stateful.tsx +++ /dev/null @@ -1,88 +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, { FC } from 'react'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageHeader, - EuiSpacer, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { ListingPropsInternal } from '..'; -import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; -import { IlmPolicyLink, MigrateIlmPolicyCallOut, ReportDiagnostic } from '../components'; -import { ReportListingTable } from '../report_listing_table'; - -/** - * Used in Stateful deployments only - * Renders controls for ILM and Screenshotting Diagnostics which are only applicable in Stateful - */ -export const ReportListingStateful: FC = (props) => { - const { apiClient, capabilities, config, navigateToUrl, toasts, urlService, ...listingProps } = - props; - const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); - const ilmPolicyContextValue = useIlmPolicyStatus(); - const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; - const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); - - return ( - <> - - } - description={ - - } - /> - - - - - - - - - - {capabilities?.management?.data?.index_lifecycle_management && ( - - {ilmPolicyContextValue?.isLoading ? ( - - ) : ( - showIlmPolicyLink && ( - - ) - )} - - )} - - - - - - ); -}; diff --git a/x-pack/platform/plugins/private/reporting/public/plugin.ts b/x-pack/platform/plugins/private/reporting/public/plugin.ts index d9589d650ffb8..a46d85ee50509 100644 --- a/x-pack/platform/plugins/private/reporting/public/plugin.ts +++ b/x-pack/platform/plugins/private/reporting/public/plugin.ts @@ -39,6 +39,9 @@ import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import type { ReportingSetup, ReportingStart } from '.'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { StartServices } from './types'; +import { APP_DESC, APP_TITLE } from './translations'; +import { APP_PATH } from './constants'; +import { setBreadcrumbs } from '@kbn/discover-plugin/public/utils/breadcrumbs'; export interface ReportingPublicPluginSetupDependencies { home: HomePublicPluginSetup; @@ -126,6 +129,7 @@ export class ReportingPublicPlugin notifications: start.notifications, rendering: start.rendering, uiSettings: start.uiSettings, + chrome: start.chrome, }, ...rest, ]; @@ -137,14 +141,10 @@ export class ReportingPublicPlugin homeSetup.featureCatalogue.register({ id: 'reporting', - title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { - defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', - }), + title: APP_TITLE, + description: APP_DESC, icon: 'reportingApp', - path: '/app/management/insightsAndAlerting/reporting', + path: APP_PATH, showOnHomePage: false, category: 'admin', }); diff --git a/x-pack/platform/plugins/private/reporting/public/translations.ts b/x-pack/platform/plugins/private/reporting/public/translations.ts new file mode 100644 index 0000000000000..878a3b28d722b --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/translations.ts @@ -0,0 +1,16 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const APP_TITLE = i18n.translate('xpack.reporting.registerFeature.reportingTitle', { + defaultMessage: 'Reporting', +}); + +export const APP_DESC = i18n.translate('xpack.reporting.registerFeature.reportingDescription', { + defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', +}); From ccba4aa4801b1dc9ae11a07dd6506fbda55ac340 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 11 Jun 2025 15:19:19 +0100 Subject: [PATCH 02/15] integrated list and disable apis --- .../private/kbn-reporting/common/types.ts | 1 + .../private/kbn-reporting/public/job.tsx | 12 + .../apis/bulk_disable_scheduled_reports.ts | 25 ++ .../apis/get_scheduled_reports_list.ts | 57 ++++ .../{ => components}/report_exports_table.tsx | 66 ++-- .../components/report_schedule_indicator.tsx | 61 ++++ .../components/report_schedules_table.tsx | 235 ++++++++++++++ .../management/components/reporting_tabs.tsx | 132 +++++--- .../suspended_component_with_props.tsx | 21 ++ .../hooks/use_bulk_disable_query.ts | 47 +++ .../hooks/use_get_scheduled_list_query.ts | 26 ++ .../reporting/public/management/index.ts | 3 +- .../management/mount_management_section.tsx | 16 +- .../reporting/public/management/query_keys.ts | 9 +- .../public/management/report_listing.test.ts | 289 ------------------ .../public/management/report_listing.tsx | 72 ----- .../private/reporting/public/plugin.ts | 1 - 17 files changed, 635 insertions(+), 438 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/apis/bulk_disable_scheduled_reports.ts create mode 100644 x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts rename x-pack/platform/plugins/private/reporting/public/management/{ => components}/report_exports_table.tsx (86%) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/suspended_component_with_props.tsx create mode 100644 x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts create mode 100644 x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts delete mode 100644 x-pack/platform/plugins/private/reporting/public/management/report_listing.test.ts delete mode 100644 x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx diff --git a/src/platform/packages/private/kbn-reporting/common/types.ts b/src/platform/packages/private/kbn-reporting/common/types.ts index b9778b11b3659..68492ec13868d 100644 --- a/src/platform/packages/private/kbn-reporting/common/types.ts +++ b/src/platform/packages/private/kbn-reporting/common/types.ts @@ -14,6 +14,7 @@ import type { import type { ConcreteTaskInstance, RruleSchedule } from '@kbn/task-manager-plugin/server'; import { JOB_STATUS } from './constants'; import type { LocatorParams } from './url'; +import { Rrule } from '@kbn/task-manager-plugin/server/task'; export * from './url'; diff --git a/src/platform/packages/private/kbn-reporting/public/job.tsx b/src/platform/packages/private/kbn-reporting/public/job.tsx index 3b4822363733e..0d403f126cd32 100644 --- a/src/platform/packages/private/kbn-reporting/public/job.tsx +++ b/src/platform/packages/private/kbn-reporting/public/job.tsx @@ -79,6 +79,7 @@ export class Job { public readonly queue_time_ms?: Required['queue_time_ms'][number]; public readonly execution_time_ms?: Required['execution_time_ms'][number]; + public readonly scheduled_report_id?: ReportSource['scheduled_report_id']; constructor(report: ReportApiJSON) { this.id = report.id; @@ -117,6 +118,7 @@ export class Job { this.metrics = report.metrics; this.queue_time_ms = report.queue_time_ms; this.execution_time_ms = report.execution_time_ms; + this.scheduled_report_id = report.scheduled_report_id; } public isSearch() { @@ -254,6 +256,16 @@ export class Job { return this.formatDate(this.created_at); } + getExportType(): string { + return this.scheduled_report_id + ? i18n.translate('reporting.jobExportType.scheduled', { + defaultMessage: 'Scheduled', + }) + : i18n.translate('reporting.jobExportType.single', { + defaultMessage: 'Single', + }); + } + /* * We use `output.warnings` to show the error of a failed report job, * and to show warnings of a job that completed with warnings. diff --git a/x-pack/platform/plugins/private/reporting/public/management/apis/bulk_disable_scheduled_reports.ts b/x-pack/platform/plugins/private/reporting/public/management/apis/bulk_disable_scheduled_reports.ts new file mode 100644 index 0000000000000..c86cdde56b0d7 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/apis/bulk_disable_scheduled_reports.ts @@ -0,0 +1,25 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import { INTERNAL_ROUTES } from '@kbn/reporting-common'; + +export const bulkDisableScheduledReports = async ({ + http, + ids = [], +}: { + http: HttpSetup; + ids: string[]; +}): Promise<{ + scheduled_report_ids: string[]; + errors: Array<{ message: string; status?: number; id: string }>; + total: number; +}> => { + return await http.patch(INTERNAL_ROUTES.SCHEDULED.BULK_DISABLE, { + body: JSON.stringify({ ids }), + }); +}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts new file mode 100644 index 0000000000000..1398a6d0c157b --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpFetchQuery, HttpSetup } from '@kbn/core/public'; +import { INTERNAL_ROUTES } from '@kbn/reporting-common'; +import { ScheduledReport } from '@kbn/reporting-common/types'; +import { type ListScheduledReportApiJSON } from '../../../server/types'; + +export interface Pagination { + index: number; + size: number; +} + +export const getScheduledReportsList = async ({ + http, + page = 0, +}: { + http: HttpSetup; + page?: number; + perPage?: number; +}): Promise<{ + page: number; + perPage: number; + total: number; + data: ScheduledReport[]; +}> => { + const query: HttpFetchQuery = { page }; + + const res = await http.get<{ + page: number; + per_page: number; + total: number; + data: ListScheduledReportApiJSON[]; + }>(INTERNAL_ROUTES.SCHEDULED.LIST, { + query, + }); + + const transformedData: ScheduledReport[] = res.data.map((item) => ({ + ...item, + createdAt: item.created_at, + createdBy: item.created_by, + lastRun: item.last_run, + nextRun: item.next_run, + objectType: item.object_type, + })); + + return { + page: res.page, + perPage: res.per_page, + total: res.total, + data: transformedData, + }; +}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx similarity index 86% rename from x-pack/platform/plugins/private/reporting/public/management/report_exports_table.tsx rename to x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 687c5943c5ed5..834653627587b 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -24,11 +24,11 @@ import { ILicense } from '@kbn/licensing-plugin/public'; import { durationToNumber, REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '@kbn/reporting-common'; import { checkLicense, Job } from '@kbn/reporting-public'; -import { ListingPropsInternal } from '.'; -import { prettyPrintJobType } from '../../common/job_utils'; -import { Poller } from '../../common/poller'; -import { ReportDeleteButton, ReportInfoFlyout, ReportStatusIndicator } from './components'; -import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from './utils'; +import { ListingPropsInternal } from '..'; +import { prettyPrintJobType } from '../../../common/job_utils'; +import { Poller } from '../../../common/poller'; +import { ReportDeleteButton, ReportInfoFlyout, ReportStatusIndicator } from '.'; +import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; type TableColumn = EuiBasicTableColumn; @@ -128,7 +128,7 @@ export class ReportExportsTable extends Component { await this.props.apiClient.deleteReport(job.id); this.removeJob(job); this.props.toasts.addSuccess( - i18n.translate('xpack.reporting.listing.table.deleteConfim', { + i18n.translate('xpack.reporting.exports.table.deleteConfirm', { defaultMessage: `The {reportTitle} report was deleted`, values: { reportTitle: job.title, @@ -137,7 +137,7 @@ export class ReportExportsTable extends Component { ); } catch (error) { this.props.toasts.addDanger( - i18n.translate('xpack.reporting.listing.table.deleteFailedErrorMessage', { + i18n.translate('xpack.reporting.exports.table.deleteFailedErrorMessage', { defaultMessage: `The report was not deleted: {error}`, values: { error }, }) @@ -183,7 +183,7 @@ export class ReportExportsTable extends Component { if (fetchError.message === 'Failed to fetch') { this.props.toasts.addDanger( fetchError.message || - i18n.translate('xpack.reporting.listing.table.requestFailedErrorMessage', { + i18n.translate('xpack.reporting.exports.table.requestFailedErrorMessage', { defaultMessage: 'Request failed', }) ); @@ -227,7 +227,7 @@ export class ReportExportsTable extends Component { { field: 'type', width: tableColumnWidths.type, - name: i18n.translate('xpack.reporting.listing.tableColumns.typeTitle', { + name: i18n.translate('xpack.reporting.exports.tableColumns.typeTitle', { defaultMessage: 'Type', }), render: (_type: string, job) => { @@ -251,8 +251,8 @@ export class ReportExportsTable extends Component { }, { field: 'title', - name: i18n.translate('xpack.reporting.listing.tableColumns.reportTitle', { - defaultMessage: 'Title', + name: i18n.translate('xpack.reporting.exports.tableColumns.reportTitle', { + defaultMessage: 'Name', }), width: tableColumnWidths.title, render: (objectTitle: string, job) => { @@ -263,7 +263,7 @@ export class ReportExportsTable extends Component { onClick={() => this.setState({ selectedJob: job })} > {objectTitle || - i18n.translate('xpack.reporting.listing.table.noTitleLabel', { + i18n.translate('xpack.reporting.exports.table.noTitleLabel', { defaultMessage: 'Untitled', })} @@ -278,7 +278,7 @@ export class ReportExportsTable extends Component { { field: 'status', width: tableColumnWidths.status, - name: i18n.translate('xpack.reporting.listing.tableColumns.statusTitle', { + name: i18n.translate('xpack.reporting.exports.tableColumns.statusTitle', { defaultMessage: 'Status', }), render: (_status: string, job) => { @@ -300,7 +300,7 @@ export class ReportExportsTable extends Component { { field: 'created_at', width: tableColumnWidths.createdAt, - name: i18n.translate('xpack.reporting.listing.tableColumns.createdAtTitle', { + name: i18n.translate('xpack.reporting.exports.tableColumns.createdAtTitle', { defaultMessage: 'Created at', }), render: (_createdAt: string, job) => ( @@ -313,7 +313,7 @@ export class ReportExportsTable extends Component { { field: 'content', width: tableColumnWidths.content, - name: i18n.translate('xpack.reporting.listing.tableColumns.content', { + name: i18n.translate('xpack.reporting.exports.tableColumns.content', { defaultMessage: 'Content', }), render: (_status: string, job) => prettyPrintJobType(job.jobtype), @@ -322,7 +322,20 @@ export class ReportExportsTable extends Component { }, }, { - name: i18n.translate('xpack.reporting.listing.tableColumns.actionsTitle', { + field: 'exportType', + width: tableColumnWidths.content, + name: i18n.translate('xpack.reporting.exports.tableColumns.exportType', { + defaultMessage: 'Export type', + }), + render: (_scheduledReportId: string, job) => { + return job.getExportType(); + }, + mobileOptions: { + show: false, + }, + }, + { + name: i18n.translate('xpack.reporting.exports.tableColumns.actionsTitle', { defaultMessage: 'Actions', }), width: tableColumnWidths.actions, @@ -332,10 +345,10 @@ export class ReportExportsTable extends Component { 'data-test-subj': (job) => `reportDownloadLink-${job.id}`, type: 'icon', icon: 'download', - name: i18n.translate('xpack.reporting.listing.table.downloadReportButtonLabel', { + name: i18n.translate('xpack.reporting.exports.table.downloadReportButtonLabel', { defaultMessage: 'Download report', }), - description: i18n.translate('xpack.reporting.listing.table.downloadReportDescription', { + description: i18n.translate('xpack.reporting.exports.table.downloadReportDescription', { defaultMessage: 'Download this report in a new tab.', }), onClick: (job) => this.props.apiClient.downloadReport(job.id), @@ -343,13 +356,13 @@ export class ReportExportsTable extends Component { }, { name: i18n.translate( - 'xpack.reporting.listing.table.viewReportingInfoActionButtonLabel', + 'xpack.reporting.exports.table.viewReportingInfoActionButtonLabel', { defaultMessage: 'View report info', } ), description: i18n.translate( - 'xpack.reporting.listing.table.viewReportingInfoActionButtonDescription', + 'xpack.reporting.exports.table.viewReportingInfoActionButtonDescription', { defaultMessage: 'View additional information about this report.', } @@ -359,12 +372,12 @@ export class ReportExportsTable extends Component { onClick: (job) => this.setState({ selectedJob: job }), }, { - name: i18n.translate('xpack.reporting.listing.table.openInKibanaAppLabel', { + name: i18n.translate('xpack.reporting.exports.table.openInKibanaAppLabel', { defaultMessage: 'Open in Kibana', }), 'data-test-subj': 'reportOpenInKibanaApp', description: i18n.translate( - 'xpack.reporting.listing.table.openInKibanaAppDescription', + 'xpack.reporting.exports.table.openInKibanaAppDescription', { defaultMessage: 'Open the Kibana app where this report was generated.', } @@ -386,7 +399,7 @@ export class ReportExportsTable extends Component { pageIndex: this.state.page, pageSize: 10, totalItemCount: this.state.total, - showPerPageOptions: false, + showPerPageOptions: true, }; const selection = { @@ -396,6 +409,7 @@ export class ReportExportsTable extends Component { return ( + {this.state.selectedJobs.length > 0 && (
@@ -405,7 +419,7 @@ export class ReportExportsTable extends Component {
)} { columns={tableColumns} noItemsMessage={ this.state.isLoading - ? i18n.translate('xpack.reporting.listing.table.loadingReportsDescription', { + ? i18n.translate('xpack.reporting.exports.table.loadingReportsDescription', { defaultMessage: 'Loading reports', }) - : i18n.translate('xpack.reporting.listing.table.noCreatedReportsDescription', { + : i18n.translate('xpack.reporting.exports.table.noCreatedReportsDescription', { defaultMessage: 'No reports have been created', }) } diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx new file mode 100644 index 0000000000000..040e3df30880a --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx @@ -0,0 +1,61 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { Frequency } from '@kbn/rrule'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { ScheduledReport } from '@kbn/reporting-common/types'; + +interface ReportScheduleIndicatorProps { + schedule: ScheduledReport['schedule']; +} + +export const ReportScheduleIndicator: FC = ({ schedule }) => { + if (!schedule || !schedule.rrule) { + return null; + } + + let statusText: string; + + switch (schedule.rrule.freq) { + case Frequency.DAILY: + statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.daily', { + defaultMessage: 'Daily', + }); + break; + case Frequency.WEEKLY: + statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.weekly', { + defaultMessage: 'Weekly', + }); + break; + case Frequency.MONTHLY: + statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.monthly', { + defaultMessage: 'Monthly', + }); + break; + default: + statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.unknown', { + defaultMessage: 'Unknown', + }); + } + + return ( + + + + + + {statusText} + + + ); +}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx new file mode 100644 index 0000000000000..bdcba53819590 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -0,0 +1,235 @@ +/* + * 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 { Fragment, default as React } from 'react'; +import { + Criteria, + EuiAvatar, + EuiBasicTable, + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiIconTip, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ScheduledReport } from '@kbn/reporting-common/types'; +import moment from 'moment'; +import { ListingPropsInternal } from '..'; +import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; +import { useGetScheduledListQuery } from '../hooks/use_get_scheduled_list_query'; +import { prettyPrintJobType } from '../../../common/job_utils'; +import { ReportScheduleIndicator } from './report_schedule_indicator'; +import { useBulkDisableQuery } from '../hooks/use_bulk_disable_query'; + +export const ReportSchedulesTable = (props: ListingPropsInternal) => { + const { http, toasts } = props; + const { data: scheduledList, isLoading } = useGetScheduledListQuery({ http }); + const { mutateAsync: bulkDisableScheduledReports } = useBulkDisableQuery({ http, toasts }); + + const tableColumns: Array> = [ + { + field: 'objectType', + name: i18n.translate('xpack.reporting.schedules.tableColumns.typeTitle', { + defaultMessage: 'Type', + }), + width: '5%', + render: (objectType: string) => ( + + ), + }, + { + field: 'title', + name: i18n.translate('xpack.reporting.schedules.tableColumns.reportTitle', { + defaultMessage: 'Title', + }), + width: '20%', + render: (title: string, item: ScheduledReport) => ( + {}}> + {title} + + ), + mobileOptions: { + header: false, + width: '100%', + }, + }, + { + field: 'status', + name: i18n.translate('xpack.reporting.schedules.tableColumns.statusTitle', { + defaultMessage: 'Status', + }), + width: '10%', + render: (status: string, item: ScheduledReport) => { + return ( + + {item.enabled + ? i18n.translate('xpack.reporting.schedules.status.active', { + defaultMessage: 'Active', + }) + : i18n.translate('xpack.reporting.schedules.status.disabled', { + defaultMessage: 'Disabled', + })} + + ); + }, + }, + { + field: 'schedule', + name: i18n.translate('xpack.reporting.schedules.tableColumns.scheduleTitle', { + defaultMessage: 'Schedule', + }), + width: '15%', + render: (schedule: ScheduledReport['schedule']) => ( + + ), + }, + { + field: 'nextRun', + name: i18n.translate('xpack.reporting.schedules.tableColumns.nextScheduleTitle', { + defaultMessage: 'Next schedule', + }), + width: '20%', + render: (nextRun: string) => { + return moment(nextRun).format('YYYY-MM-DD @ hh:mm A'); + }, + }, + { + field: 'jobtype', + width: '10%', + name: i18n.translate('xpack.reporting.schedules.tableColumns.fileType', { + defaultMessage: 'File Type', + }), + render: (jobtype: string) => prettyPrintJobType(jobtype), + mobileOptions: { + show: false, + }, + }, + { + field: 'createdBy', + name: i18n.translate('xpack.reporting.schedules.tableColumns.createdByTitle', { + defaultMessage: 'Created by', + }), + width: '15%', + render: (createdBy: string) => { + return ( + + + + + + + {createdBy} + + + + ); + }, + }, + { + field: 'actions', + name: i18n.translate('xpack.reporting.schedules.tableColumns.actionsTitle', { + defaultMessage: 'Actions', + }), + width: '5%', + actions: [ + { + name: i18n.translate('xpack.reporting.schedules.table.viewConfig.title', { + defaultMessage: 'View schedule config', + }), + description: i18n.translate('xpack.reporting.schedules.table.viewConfig.description', { + defaultMessage: 'View schedule configuration details', + }), + 'data-test-subj': 'reportViewConfig', + type: 'icon', + icon: 'calendar', + onClick: () => {}, + }, + { + name: i18n.translate('xpack.reporting.schedules.table.openDashboard.title', { + defaultMessage: 'Open Dashboard', + }), + description: i18n.translate('xpack.reporting.schedules.table.openDashboard.description', { + defaultMessage: 'Open associated dashboard', + }), + 'data-test-subj': 'reportOpenDashboard', + type: 'icon', + icon: 'dashboardApp', + + onClick: (item) => { + // const searchParams = stringify({ id: item.id }); + // const path = buildKibanaPath({ + // basePath: http.basePath.serverBasePath, + // // spaceId: .spaceId, + // appPath: REPORTING_REDIRECT_APP, + // }); + // const href = `${path}?${searchParams}`; + // window.open(href, '_blank'); + // window.focus(); + }, + }, + { + name: i18n.translate('xpack.reporting.schedules.table.disableSchedule.title', { + defaultMessage: 'Disable schedule', + }), + description: i18n.translate( + 'xpack.reporting.schedules.table.disableSchedule.description', + { + defaultMessage: 'Disable report schedule', + } + ), + 'data-test-subj': 'reportDisableSchedule', + enabled: (item) => item.enabled, + type: 'icon', + icon: 'cross', + onClick: (item) => { + bulkDisableScheduledReports({ ids: [item.id] }); + }, + }, + ], + }, + ]; + + return ( + + + ) => {}} + /> + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ReportSchedulesTable as default }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx index 8c3cca2464164..35afb30521dc4 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -5,22 +5,30 @@ * 2.0. */ -import React, { lazy, useCallback, useEffect } from 'react'; -import { EuiPageHeader, EuiPageTemplate, EuiTab, EuiTabs } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { REPORTING_EXPORTS_PATH, REPORTING_SCHEDULES_PATH, Section } from '../../constants'; import { Route, Routes } from '@kbn/shared-ux-router'; -import { suspendedComponentWithProps } from '../../suspended_component_with_props'; -import { RouteComponentProps } from 'react-router'; +import { RouteComponentProps } from 'react-router-dom'; import { CoreStart, ScopedHistory } from '@kbn/core/public'; import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { ClientConfigType, ReportingAPIClient, useKibana } from '@kbn/reporting-public'; +import { + ClientConfigType, + ReportingAPIClient, + useInternalApiClient, + useKibana, +} from '@kbn/reporting-public'; import { SharePluginStart } from '@kbn/share-plugin/public'; -import { getReportingSectionBreadcrumb } from './get_reporting_sections_breadcrumb'; import { FormattedMessage } from '@kbn/i18n-react'; - -const ReportListing = lazy(() => import('../report_listing')); +import { suspendedComponentWithProps } from './suspended_component_with_props'; +import { REPORTING_EXPORTS_PATH, REPORTING_SCHEDULES_PATH, Section } from '../../constants'; +import ReportExportsTable from './report_exports_table'; +import { IlmPolicyLink } from './ilm_policy_link'; +import { ReportDiagnostic } from './report_diagnostic'; +import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; +import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +import ReportSchedulesTable from './report_schedules_table'; export interface MatchParams { section: Section; @@ -38,10 +46,23 @@ export interface ReportingTabsProps { export const ReportingTabs: React.FunctionComponent< Partial & ReportingTabsProps > = (props) => { - const { coreStart, license$, shareService, config, apiClient, ...rest } = props; - const { application, notifications } = coreStart; + const { coreStart, license$, shareService, config, ...rest } = props; + const { notifications } = coreStart; const { section } = rest.match?.params as MatchParams; const history = rest.history as ScopedHistory; + const { apiClient } = useInternalApiClient(); + const { + services: { + application: { capabilities, navigateToApp, navigateToUrl }, + http, + }, + } = useKibana(); + + const ilmLocator = shareService.url.locators.get('ILM_LOCATOR_ID'); + const ilmPolicyContextValue = useIlmPolicyStatus(); + const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; + const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); + const tabs = [ { id: 'exports', @@ -59,38 +80,61 @@ export const ReportingTabs: React.FunctionComponent< const renderExportsList = useCallback(() => { return suspendedComponentWithProps( - ReportListing, - + ReportExportsTable, 'xl' )({ apiClient, toasts: notifications.toasts, license$, config, - redirect: application.navigateToApp, - navigateToUrl: application.navigateToUrl, + capabilities, + redirect: navigateToApp, + navigateToUrl, urlService: shareService.url, + http, }); - }, []); + }, [ + apiClient, + notifications.toasts, + license$, + config, + capabilities, + navigateToApp, + navigateToUrl, + shareService.url, + http, + ]); const renderSchedulesList = useCallback(() => { return ( {suspendedComponentWithProps( - ReportListing, + ReportSchedulesTable, 'xl' )({ apiClient, toasts: notifications.toasts, license$, config, - redirect: application.navigateToApp, - navigateToUrl: application.navigateToUrl, + capabilities, + redirect: navigateToApp, + navigateToUrl, urlService: shareService.url, + http, })} ); - }, []); + }, [ + apiClient, + notifications.toasts, + license$, + config, + capabilities, + navigateToApp, + navigateToUrl, + shareService.url, + http, + ]); const onSectionChange = (newSection: Section) => { history.push(`/${newSection}`); @@ -98,9 +142,33 @@ export const ReportingTabs: React.FunctionComponent< return ( <> - , + + {capabilities?.management?.data?.index_lifecycle_management && ( + + {ilmPolicyContextValue?.isLoading ? ( + + ) : ( + showIlmPolicyLink && ( + + ) + )} + + )}{' '} + , + + + , + ] + : [] + } + data-test-subj="reportingPageHeader" pageTitle={ } + tabs={tabs.map(({ id, name }) => ({ + label: name, + onClick: () => onSectionChange(id as Section), + isSelected: id === section, + key: id, + 'data-test-subj': `reportingTabs-${id}`, + }))} /> - - {tabs.map(({ id, name }) => ( - onSectionChange(id as Section)} - isSelected={section === id} - data-test-subj={`connectionDetailsTabBtn-${id}`} - > - {name} - - ))} - - diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/suspended_component_with_props.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/suspended_component_with_props.tsx new file mode 100644 index 0000000000000..994aa399b177d --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/suspended_component_with_props.tsx @@ -0,0 +1,21 @@ +/* + * 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, { Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +export function suspendedComponentWithProps( + ComponentToSuspend: React.ComponentType, + size?: 's' | 'm' | 'l' | 'xl' | 'xxl' +) { + return (props: T) => ( + }> + {/* @ts-expect-error upgrade typescript v4.9.5*/} + + + ); +} diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts new file mode 100644 index 0000000000000..45e27ce9ce741 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { HttpSetup, IHttpFetchError, ResponseErrorBody, ToastsStart } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { bulkDisableScheduledReports } from '../apis/bulk_disable_scheduled_reports'; +import { mutationKeys, queryKeys } from '../query_keys'; + +export type ServerError = IHttpFetchError; + +const getKey = mutationKeys.bulkDisableScheduledReports; + +export const useBulkDisableQuery = (props: { http: HttpSetup; toasts: ToastsStart }) => { + const { http, toasts } = props; + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: getKey(), + mutationFn: ({ ids }: { ids: string[] }) => + bulkDisableScheduledReports({ + http, + ids, + }), + onError: (error: ServerError) => { + toasts.addError(error, { + title: i18n.translate('xpack.reporting.schedules.reports.disableError', { + defaultMessage: 'Error disabling scheduled report', + }), + }); + }, + onSuccess: () => { + toasts.addSuccess( + i18n.translate('xpack.reporting.schedules.reports.disabled', { + defaultMessage: 'Scheduled report disabled', + }) + ); + queryClient.invalidateQueries({ + queryKey: queryKeys.getScheduledList(), + }); + }, + }); +}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts new file mode 100644 index 0000000000000..5156546f5b153 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts @@ -0,0 +1,26 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { HttpSetup } from '@kbn/core/public'; +import { getScheduledReportsList } from '../apis/get_scheduled_reports_list'; +import { queryKeys } from '../query_keys'; + +export const getKey = queryKeys.getScheduledList; + +interface GetScheduledListQueryProps { + http: HttpSetup; + page?: number; + perPage?: number; +} + +export const useGetScheduledListQuery = (props: GetScheduledListQueryProps) => { + return useQuery({ + queryKey: getKey(), + queryFn: () => getScheduledReportsList(props), + }); +}; diff --git a/x-pack/platform/plugins/private/reporting/public/management/index.ts b/x-pack/platform/plugins/private/reporting/public/management/index.ts index 09c4517e304e0..1f5fce0f9efa0 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/index.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ApplicationStart, ToastsStart } from '@kbn/core/public'; +import type { ApplicationStart, HttpSetup, ToastsStart } from '@kbn/core/public'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { ClientConfigType, ReportingAPIClient } from '@kbn/reporting-public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; @@ -21,6 +21,7 @@ export interface ListingProps { export type ListingPropsInternal = ListingProps & { capabilities: ApplicationStart['capabilities']; + http: HttpSetup; }; export { ReportListing } from './report_listing'; diff --git a/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx b/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx index e59322a48224d..5a681c889ed7c 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/mount_management_section.tsx @@ -24,11 +24,11 @@ import { import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '../query_client'; -import { PolicyStatusContextProvider } from '../lib/default_status_context'; -import { Section } from '../constants'; -import { Route, Router, Routes } from '@kbn/shared-ux-router'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { Redirect } from 'react-router'; +import { Route, Router, Routes } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; +import { Section } from '../constants'; +import { PolicyStatusContextProvider } from '../lib/default_status_context'; const ReportingTabs = lazy(() => import('./components/reporting_tabs')); @@ -80,14 +80,6 @@ export async function mountManagementSection({ { - console.log('Redirecting to Reporting Home', { - routerProps: params, - history, - coreStart, - config, - apiClient, - license$, - }); return ( }> [queryKeys.root, 'health'] as const, + getScheduledList: () => [root, 'scheduledList'], + getHealth: () => [root, 'health'] as const, +}; + +export const mutationKeys = { + bulkDisableScheduledReports: () => [root, 'bulkDisableScheduledReports'] as const, }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/report_listing.test.ts b/x-pack/platform/plugins/private/reporting/public/management/report_listing.test.ts deleted file mode 100644 index 8f0dd9bf17a7f..0000000000000 --- a/x-pack/platform/plugins/private/reporting/public/management/report_listing.test.ts +++ /dev/null @@ -1,289 +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 { act } from 'react-dom/test-utils'; -import type { Observable } from 'rxjs'; - -import type { ILicense } from '@kbn/licensing-plugin/public'; -import { IlmPolicyMigrationStatus } from '@kbn/reporting-common/types'; - -import { ListingProps as Props } from '.'; -import { mockJobs } from '../../common/test'; -import { TestBed, TestDependencies, setup } from './__test__'; -import { mockConfig } from './__test__/report_listing.test.helpers'; -import { Job } from '@kbn/reporting-public'; - -describe('ReportListing', () => { - let testBed: TestBed; - let applicationService: TestDependencies['application']; - - const runSetup = async (props?: Partial) => { - await act(async () => { - testBed = await setup(props); - }); - testBed.component.update(); - }; - - beforeEach(async () => { - await runSetup(); - // Collect all of the injected services so we can mutate for the tests - applicationService = testBed.testDependencies.application; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders a listing with some items', () => { - const { find } = testBed; - expect(find('reportJobRow').length).toBe(mockJobs.length); - }); - - it('subscribes to license changes, and unsubscribes on dismount', async () => { - const unsubscribeMock = jest.fn(); - const subMock = { - subscribe: jest.fn().mockReturnValue({ - unsubscribe: unsubscribeMock, - }), - } as unknown as Observable; - - await runSetup({ license$: subMock }); - - expect(subMock.subscribe).toHaveBeenCalled(); - expect(unsubscribeMock).not.toHaveBeenCalled(); - testBed.component.unmount(); - 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('flyout', () => { - let reportingAPIClient: TestDependencies['reportingAPIClient']; - let jobUnderTest: Job; - - beforeEach(async () => { - await runSetup(); - reportingAPIClient = testBed.testDependencies.reportingAPIClient; - jest.spyOn(reportingAPIClient, 'getInfo').mockResolvedValue(jobUnderTest); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('shows the enabled "open in Kibana" button in the actions menu for v2 jobs', async () => { - const [jobJson] = mockJobs; - jobUnderTest = new Job(jobJson); - const { actions } = testBed; - - await actions.flyout.open(jobUnderTest.id); - actions.flyout.openActionsMenu(); - expect(actions.flyout.findOpenInAppButton().props().disabled).toBe(false); - }); - - it('shows the disabled "open in Kibana" button in the actions menu for pre-v2 jobs', async () => { - const [, jobJson] = mockJobs; - jobUnderTest = new Job(jobJson); - const { actions } = testBed; - - await actions.flyout.open(jobUnderTest.id); - actions.flyout.openActionsMenu(); - expect(actions.flyout.findOpenInAppButton().props().disabled).toBe(true); - }); - - it('shows the disabled "Download" button in the actions menu for a job that is not done', async () => { - const [jobJson] = mockJobs; - jobUnderTest = new Job(jobJson); - const { actions } = testBed; - - await actions.flyout.open(jobUnderTest.id); - actions.flyout.openActionsMenu(); - expect(actions.flyout.findDownloadButton().props().disabled).toBe(true); - }); - - it('shows the enabled "Download" button in the actions menu for a job is done', async () => { - const [, , jobJson] = mockJobs; - jobUnderTest = new Job(jobJson); - const { actions } = testBed; - - await actions.flyout.open(jobUnderTest.id); - actions.flyout.openActionsMenu(); - expect(actions.flyout.findDownloadButton().props().disabled).toBe(false); - }); - }); - - describe('ILM policy', () => { - let httpService: TestDependencies['http']; - let urlService: TestDependencies['urlService']; - let toasts: TestDependencies['toasts']; - let reportingAPIClient: TestDependencies['reportingAPIClient']; - - /** - * Simulate a fresh page load, useful for network requests and other effects - * that happen only at first load. - */ - const remountComponent = async () => { - const { component } = testBed; - act(() => { - component.unmount(); - }); - await act(async () => { - component.mount(); - }); - // Flush promises - await new Promise((r) => setImmediate(r)); - component.update(); - }; - - beforeEach(async () => { - await runSetup(); - // Collect all of the injected services so we can mutate for the tests - applicationService = testBed.testDependencies.application; - applicationService.capabilities = { - catalogue: {}, - navLinks: {}, - management: { data: { index_lifecycle_management: true } }, - }; - httpService = testBed.testDependencies.http; - urlService = testBed.testDependencies.urlService; - toasts = testBed.testDependencies.toasts; - reportingAPIClient = testBed.testDependencies.reportingAPIClient; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('shows the migrate banner when migration status is not "OK"', async () => { - const { actions } = testBed; - const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - httpService.get.mockResolvedValue({ status }); - await remountComponent(); - expect(actions.hasIlmMigrationBanner()).toBe(true); - }); - - it('does not show the migrate banner when migration status is "OK"', async () => { - const { actions } = testBed; - const status: IlmPolicyMigrationStatus = 'ok'; - httpService.get.mockResolvedValue({ status }); - await remountComponent(); - expect(actions.hasIlmMigrationBanner()).toBe(false); - }); - - it('hides the ILM policy link if there is no ILM policy', async () => { - const { actions } = testBed; - const status: IlmPolicyMigrationStatus = 'policy-not-found'; - httpService.get.mockResolvedValue({ status }); - await remountComponent(); - expect(actions.hasIlmPolicyLink()).toBe(false); - }); - - it('hides the ILM policy link if there is no ILM policy locator', async () => { - const { actions } = testBed; - jest.spyOn(urlService.locators, 'get').mockReturnValue(undefined); - const status: IlmPolicyMigrationStatus = 'ok'; // should never happen, but need to test that when the locator is missing we don't render the link - httpService.get.mockResolvedValue({ status }); - await remountComponent(); - expect(actions.hasIlmPolicyLink()).toBe(false); - }); - - it('always shows the ILM policy link if there is an ILM policy', async () => { - const { actions } = testBed; - const status: IlmPolicyMigrationStatus = 'ok'; - httpService.get.mockResolvedValue({ status }); - await remountComponent(); - expect(actions.hasIlmPolicyLink()).toBe(true); - - const status2: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - httpService.get.mockResolvedValue({ status: status2 }); - await remountComponent(); - expect(actions.hasIlmPolicyLink()).toBe(true); - }); - - it('hides the banner after migrating indices', async () => { - const { actions } = testBed; - const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - const status2: IlmPolicyMigrationStatus = 'ok'; - httpService.get.mockResolvedValueOnce({ status }); - httpService.get.mockResolvedValueOnce({ status: status2 }); - await remountComponent(); - - expect(actions.hasIlmMigrationBanner()).toBe(true); - await actions.migrateIndices(); - expect(actions.hasIlmMigrationBanner()).toBe(false); - expect(actions.hasIlmPolicyLink()).toBe(true); - expect(toasts.addSuccess).toHaveBeenCalledTimes(1); - }); - - it('informs users when migrations failed', async () => { - const { actions } = testBed; - const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - httpService.get.mockResolvedValueOnce({ status }); - (reportingAPIClient.migrateReportingIndicesIlmPolicy as jest.Mock).mockRejectedValueOnce( - new Error('oops!') - ); - await remountComponent(); - - expect(actions.hasIlmMigrationBanner()).toBe(true); - await actions.migrateIndices(); - expect(toasts.addError).toHaveBeenCalledTimes(1); - expect(actions.hasIlmMigrationBanner()).toBe(true); - expect(actions.hasIlmPolicyLink()).toBe(true); - }); - - it('only shows the link to the ILM policy if UI capabilities allow it', async () => { - applicationService.capabilities = { - catalogue: {}, - navLinks: {}, - management: { data: { index_lifecycle_management: false } }, - }; - await remountComponent(); - - expect(testBed.actions.hasIlmPolicyLink()).toBe(false); - - applicationService.capabilities = { - catalogue: {}, - navLinks: {}, - management: { data: { index_lifecycle_management: true } }, - }; - - await remountComponent(); - - expect(testBed.actions.hasIlmPolicyLink()).toBe(true); - }); - }); - describe('Screenshotting Diagnostic', () => { - it('shows screenshotting diagnostic link if config enables image reports', () => { - expect(testBed.actions.hasScreenshotDiagnosticLink()).toBe(true); - }); - it('does not show when image reporting not set in config', async () => { - const mockNoImageConfig = { - ...mockConfig, - export_types: { - csv: { enabled: true }, - pdf: { enabled: false }, - png: { enabled: false }, - }, - }; - await runSetup({ config: mockNoImageConfig }); - expect(testBed.actions.hasScreenshotDiagnosticLink()).toBe(false); - }); - }); -}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx b/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx deleted file mode 100644 index b25d926a32b60..0000000000000 --- a/x-pack/platform/plugins/private/reporting/public/management/report_listing.tsx +++ /dev/null @@ -1,72 +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 { useInternalApiClient, useKibana } from '@kbn/reporting-public'; -import { ListingProps } from '.'; -import { IlmPolicyLink, MigrateIlmPolicyCallOut, ReportDiagnostic } from './components'; -import { ReportExportsTable } from './report_exports_table'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; -import { useIlmPolicyStatus } from '../lib/ilm_policy_status_context'; - -export const ReportListing = (props: ListingProps) => { - console.log('Inside report listing', { props }); - const { apiClient } = useInternalApiClient(); - const { - services: { - application: { capabilities }, - }, - } = useKibana(); - - const { config, navigateToUrl, toasts, urlService, ...listingProps } = props; - const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); - const ilmPolicyContextValue = useIlmPolicyStatus(); - const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; - const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); - return ( - <> - {props.config.statefulSettings.enabled ? : null} - - - - - - {props.config.statefulSettings.enabled ? ( - <> - - - {capabilities?.management?.data?.index_lifecycle_management && ( - - {ilmPolicyContextValue?.isLoading ? ( - - ) : ( - showIlmPolicyLink && ( - - ) - )} - - )} - - - - - - ) : null} - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { ReportListing as default }; diff --git a/x-pack/platform/plugins/private/reporting/public/plugin.ts b/x-pack/platform/plugins/private/reporting/public/plugin.ts index a46d85ee50509..4eb247b5f17d8 100644 --- a/x-pack/platform/plugins/private/reporting/public/plugin.ts +++ b/x-pack/platform/plugins/private/reporting/public/plugin.ts @@ -41,7 +41,6 @@ import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_ha import { StartServices } from './types'; import { APP_DESC, APP_TITLE } from './translations'; import { APP_PATH } from './constants'; -import { setBreadcrumbs } from '@kbn/discover-plugin/public/utils/breadcrumbs'; export interface ReportingPublicPluginSetupDependencies { home: HomePublicPluginSetup; From 49e18fce4e5538c80bc80f259521efd864918dd2 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 13 Jun 2025 12:53:33 +0100 Subject: [PATCH 03/15] added tests --- .../private/kbn-reporting/public/job.tsx | 24 +- .../apis/get_scheduled_reports_list.ts | 13 +- .../components/report_exports_table.tsx | 10 +- .../components/report_schedules_table.tsx | 48 +++- .../components/reporting_tabs.test.tsx | 271 ++++++++++++++++++ .../hooks/use_bulk_disable.test.tsx | 76 +++++ ..._disable_query.ts => use_bulk_disable.tsx} | 5 +- .../hooks/use_get_scheduled_list.test.tsx | 48 ++++ ...st_query.ts => use_get_scheduled_list.tsx} | 10 +- .../reporting/public/management/index.ts | 2 +- .../reporting/public/management/query_keys.ts | 2 +- 11 files changed, 477 insertions(+), 32 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx create mode 100644 x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx rename x-pack/platform/plugins/private/reporting/public/management/hooks/{use_bulk_disable_query.ts => use_bulk_disable.tsx} (89%) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx rename x-pack/platform/plugins/private/reporting/public/management/hooks/{use_get_scheduled_list_query.ts => use_get_scheduled_list.tsx} (75%) diff --git a/src/platform/packages/private/kbn-reporting/public/job.tsx b/src/platform/packages/private/kbn-reporting/public/job.tsx index 0d403f126cd32..bb300d574938a 100644 --- a/src/platform/packages/private/kbn-reporting/public/job.tsx +++ b/src/platform/packages/private/kbn-reporting/public/job.tsx @@ -10,7 +10,14 @@ import moment from 'moment'; import React from 'react'; -import { EuiText, EuiTextColor } from '@elastic/eui'; +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiText, + EuiTextColor, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JOB_STATUS } from '@kbn/reporting-common'; import type { @@ -256,14 +263,25 @@ export class Job { return this.formatDate(this.created_at); } - getExportType(): string { - return this.scheduled_report_id + getExportType(): React.ReactElement { + const exportType = this.scheduled_report_id ? i18n.translate('reporting.jobExportType.scheduled', { defaultMessage: 'Scheduled', }) : i18n.translate('reporting.jobExportType.single', { defaultMessage: 'Single', }); + + return ( + + + + + + {exportType} + + + ); } /* diff --git a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts index 1398a6d0c157b..3fdabcc90d50b 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts @@ -17,18 +17,19 @@ export interface Pagination { export const getScheduledReportsList = async ({ http, - page = 0, + index, + size, }: { http: HttpSetup; - page?: number; - perPage?: number; + index?: number; + size?: number; }): Promise<{ page: number; - perPage: number; + size: number; total: number; data: ScheduledReport[]; }> => { - const query: HttpFetchQuery = { page }; + const query: HttpFetchQuery = { page: index, size }; const res = await http.get<{ page: number; @@ -50,7 +51,7 @@ export const getScheduledReportsList = async ({ return { page: res.page, - perPage: res.per_page, + size: res.per_page, total: res.total, data: transformedData, }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 834653627587b..58003939e7f0e 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -172,6 +172,7 @@ export class ReportExportsTable extends Component { try { jobs = await this.props.apiClient.list(this.state.page); total = await this.props.apiClient.total(); + this.isInitialJobsFetch = false; } catch (fetchError) { if (!this.licenseAllowsToShowThisPage()) { @@ -216,9 +217,10 @@ export class ReportExportsTable extends Component { type: '5%', title: '30%', status: '20%', - createdAt: '25%', - content: '10%', - actions: '10%', + createdAt: '21%', + content: '7%', + exportType: '12%', + actions: '5%', }; public render() { @@ -323,7 +325,7 @@ export class ReportExportsTable extends Component { }, { field: 'exportType', - width: tableColumnWidths.content, + width: tableColumnWidths.exportType, name: i18n.translate('xpack.reporting.exports.tableColumns.exportType', { defaultMessage: 'Export type', }), diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index bdcba53819590..e1b606ac1b56c 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { Fragment, default as React } from 'react'; +import { Fragment, default as React, useCallback, useState } from 'react'; import { - Criteria, EuiAvatar, EuiBasicTable, EuiBasicTableColumn, @@ -25,15 +24,32 @@ import { ScheduledReport } from '@kbn/reporting-common/types'; import moment from 'moment'; import { ListingPropsInternal } from '..'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; -import { useGetScheduledListQuery } from '../hooks/use_get_scheduled_list_query'; +import { useGetScheduledList } from '../hooks/use_get_scheduled_list'; import { prettyPrintJobType } from '../../../common/job_utils'; import { ReportScheduleIndicator } from './report_schedule_indicator'; -import { useBulkDisableQuery } from '../hooks/use_bulk_disable_query'; +import { useBulkDisable } from '../hooks/use_bulk_disable'; + +interface QueryParams { + index: number; + size: number; +} export const ReportSchedulesTable = (props: ListingPropsInternal) => { const { http, toasts } = props; - const { data: scheduledList, isLoading } = useGetScheduledListQuery({ http }); - const { mutateAsync: bulkDisableScheduledReports } = useBulkDisableQuery({ http, toasts }); + const [queryParams, setQueryParams] = useState({ + index: 1, + size: 10, + }); + const { data: scheduledList, isLoading } = useGetScheduledList({ + http, + ...queryParams, + }); + + const { mutateAsync: bulkDisableScheduledReports } = useBulkDisable({ + http, + toasts, + ...queryParams, + }); const tableColumns: Array> = [ { @@ -211,6 +227,17 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { }, ]; + const tableOnChangeCallback = useCallback( + ({ page }: { page: QueryParams }) => { + setQueryParams((prev) => ({ + ...prev, + index: page.index + 1, + size: page.size, + })); + }, + [setQueryParams] + ); + return ( @@ -220,12 +247,11 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { columns={tableColumns} loading={isLoading} pagination={{ - pageIndex: scheduledList?.page || 0, - pageSize: scheduledList?.perPage || 10, - totalItemCount: scheduledList?.total || 0, - showPerPageOptions: true, + pageIndex: queryParams.index - 1, + pageSize: queryParams.size, + totalItemCount: scheduledList?.total ?? 0, }} - onChange={(criteria: Criteria) => {}} + onChange={tableOnChangeCallback} /> ); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx new file mode 100644 index 0000000000000..dcbb0013810f7 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx @@ -0,0 +1,271 @@ +/* + * 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 * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { RouteComponentProps } from 'react-router-dom'; +import { Router } from '@kbn/shared-ux-router'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { createMemoryHistory, createLocation } from 'history'; + +import ReportingTabs, { MatchParams, ReportingTabsProps } from './reporting_tabs'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + applicationServiceMock, + coreMock, + httpServiceMock, + notificationServiceMock, +} from '@kbn/core/public/mocks'; +import { InternalApiClientProvider, ReportingAPIClient } from '@kbn/reporting-public'; +import { Observable } from 'rxjs'; +import { ILicense } from '@kbn/licensing-plugin/public'; +import { LocatorPublic, SharePluginSetup } from '@kbn/share-plugin/public'; +import { SerializableRecord } from '@kbn/utility-types'; +import { ReportDiagnostic } from './report_diagnostic'; +import { mockConfig } from '../__test__/report_listing.test.helpers'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { EuiThemeProvider } from '@elastic/eui'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { IlmPolicyStatusContextProvider } from '../../lib/ilm_policy_status_context'; +import { dataService } from '@kbn/controls-plugin/public/services/kibana_services'; +import { shareService } from '@kbn/dashboard-plugin/public/services/kibana_services'; +import { IlmPolicyMigrationStatus } from '@kbn/reporting-common/types'; +import { HttpSetupMock } from '@kbn/core-http-browser-mocks'; +import { act } from 'react-dom/test-utils'; + +jest.mock('./report_exports_table', () => { + return () =>
{'Render Report Exports Table'}
; +}); + +jest.mock('./report_schedules_table', () => { + return () =>
{'Render Report Schedules Table'}
; +}); + +const queryClient = new QueryClient(); + +describe('Reporting tabs', () => { + const ilmLocator: LocatorPublic = { + getUrl: jest.fn(), + } as unknown as LocatorPublic; + const http = httpServiceMock.createSetupContract(); + const uiSettingsClient = coreMock.createSetup().uiSettings; + const httpService = httpServiceMock.createSetupContract(); + const application = applicationServiceMock.createStartContract(); + const reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); + const validCheck = { + check: () => ({ + state: 'VALID', + message: '', + }), + }; + const license$ = { + subscribe: (handler: unknown) => { + return (handler as Function)(validCheck); + }, + } as Observable; + + const reportDiagnostic = () => ( + + ); + + const routeProps: RouteComponentProps = { + history: createMemoryHistory({ + initialEntries: ['/exports'], + }), + location: createLocation('/exports'), + match: { + isExact: true, + path: `/exports`, + url: '', + params: { + section: 'exports', + }, + }, + }; + + const props = { + ...routeProps, + coreStart: coreMock.createStart(), + http, + application, + apiClient: reportingAPIClient, + config: mockConfig, + license$, + urlService: { + locators: { + get: () => ilmLocator, + }, + } as unknown as SharePluginSetup['url'], + toasts: notificationServiceMock.createSetupContract().toasts, + ilmLocator, + uiSettings: uiSettingsClient, + reportDiagnostic, + dataService: dataPluginMock.createStartContract(), + shareService: sharePluginMock.createStartContract(), + }; + + const renderComponent = ( + renderProps: Partial & ReportingTabsProps, + newHttpService?: HttpSetupMock + ) => { + const updatedReportingAPIClient = newHttpService + ? new ReportingAPIClient(newHttpService, uiSettingsClient, 'x.x.x') + : reportingAPIClient; + return ( + + + + + + + + + + + + + + + + ); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders exports components', async () => { + await act(async () => render(renderComponent(props))); + + expect(await screen.findByTestId('reportingTabs-exports')).toBeInTheDocument(); + expect(await screen.findByTestId('reportingTabs-schedules')).toBeInTheDocument(); + }); + + it('shows the correct number of tabs', async () => { + const updatedProps: RouteComponentProps = { + history: createMemoryHistory(), + location: createLocation('/'), + match: { + isExact: true, + path: `/schedules`, + url: '', + params: { + section: 'schedules', + }, + }, + }; + + await act(async () => { + render(renderComponent({ ...props, ...updatedProps })); + }); + + expect(await screen.findAllByRole('tab')).toHaveLength(2); + }); + + describe('ILM policy', () => { + it('shows ILM policy link correctly when config is stateful', async () => { + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + + application.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: true } }, + }; + + const updatedShareService = { + ...sharePluginMock.createStartContract(), + url: { + ...sharePluginMock.createStartContract().url, + locators: { + ...sharePluginMock.createStartContract().url.locators, + id: 'ILM_LOCATOR_ID', + get: () => ilmLocator, + }, + }, + }; + + await act(async () => { + // @ts-expect-error we don't need to provide all props for the test + render(renderComponent({ ...props, shareService: updatedShareService })); + }); + + expect(await screen.findByTestId('ilmPolicyLink')).toBeInTheDocument(); + }); + + it('hides ILM policy link correctly for non stateful config', async () => { + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + + application.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: true } }, + }; + + const updatedShareService = { + ...sharePluginMock.createStartContract(), + url: { + ...sharePluginMock.createStartContract().url, + locators: { + ...sharePluginMock.createStartContract().url.locators, + id: 'ILM_LOCATOR_ID', + get: () => ilmLocator, + }, + }, + }; + const newConfig = { ...mockConfig, statefulSettings: { enabled: false } }; + + await act(async () => { + // @ts-expect-error we don't need to provide all props for the test + render(renderComponent({ ...props, shareService: updatedShareService, config: newConfig })); + }); + + expect(screen.queryByTestId('ilmPolicyLink')).not.toBeInTheDocument(); + }); + }); + + describe('Screenshotting Diagnostic', () => { + it('shows screenshotting diagnostic link if config is stateful', async () => { + await act(async () => { + render(renderComponent(props)); + }); + + expect(await screen.findByTestId('screenshotDiagnosticLink')).toBeInTheDocument(); + }); + it('does not show when image reporting not set in config', async () => { + const mockNoImageConfig = { + ...mockConfig, + export_types: { + csv: { enabled: true }, + pdf: { enabled: false }, + png: { enabled: false }, + }, + }; + + await act(async () => { + render( + renderComponent({ + ...props, + config: mockNoImageConfig, + }) + ); + }); + + expect(screen.queryByTestId('screenshotDiagnosticLink')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx new file mode 100644 index 0000000000000..7cfd82a51fa17 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { httpServiceMock, notificationServiceMock } from '@kbn/core/public/mocks'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import { useBulkDisable } from './use_bulk_disable'; +import { bulkDisableScheduledReports } from '../apis/bulk_disable_scheduled_reports'; + +jest.mock('../apis/bulk_disable_scheduled_reports', () => ({ + bulkDisableScheduledReports: jest.fn(), +})); + +describe('useBulkDisable', () => { + const http = httpServiceMock.createStartContract(); + const toasts = notificationServiceMock.createStartContract().toasts; + const queryClient = new QueryClient(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls bulkDisableScheduledReports with correct arguments', async () => { + (bulkDisableScheduledReports as jest.Mock).mockResolvedValueOnce({ + scheduled_report_ids: ['random_schedule_report_1'], + errors: [], + total: 1, + }); + + const { result } = renderHook(() => useBulkDisable({ http, toasts }), { + wrapper, + }); + + result.current.mutate({ ids: ['random_schedule_report_1'] }); + + await waitFor(() => { + expect(bulkDisableScheduledReports).toBeCalledWith({ + http, + ids: ['random_schedule_report_1'], + }); + expect(result.current.data).toEqual({ + scheduled_report_ids: ['random_schedule_report_1'], + errors: [], + total: 1, + }); + expect(toasts.addSuccess).toHaveBeenCalled(); + }); + }); + + it('throws error', async () => { + (bulkDisableScheduledReports as jest.Mock).mockRejectedValueOnce({}); + + const { result } = renderHook(() => useBulkDisable({ http, toasts }), { + wrapper, + }); + + result.current.mutate({ ids: [] }); + + await waitFor(() => { + expect(bulkDisableScheduledReports).toBeCalledWith({ + http, + ids: [], + }); + expect(toasts.addError).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx similarity index 89% rename from x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts rename to x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx index 45e27ce9ce741..b0ab70d05093d 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable_query.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx @@ -15,7 +15,7 @@ export type ServerError = IHttpFetchError; const getKey = mutationKeys.bulkDisableScheduledReports; -export const useBulkDisableQuery = (props: { http: HttpSetup; toasts: ToastsStart }) => { +export const useBulkDisable = (props: { http: HttpSetup; toasts: ToastsStart }) => { const { http, toasts } = props; const queryClient = useQueryClient(); @@ -40,7 +40,8 @@ export const useBulkDisableQuery = (props: { http: HttpSetup; toasts: ToastsStar }) ); queryClient.invalidateQueries({ - queryKey: queryKeys.getScheduledList(), + queryKey: queryKeys.getScheduledList({}), + refetchType: 'active', }); }, }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx new file mode 100644 index 0000000000000..8a7fff87387e7 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/public/mocks'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import { getScheduledReportsList } from '../apis/get_scheduled_reports_list'; +import { useGetScheduledList } from './use_get_scheduled_list'; + +jest.mock('../apis/get_scheduled_reports_list', () => ({ + getScheduledReportsList: jest.fn(), +})); + +describe('useGetScheduledList', () => { + const http = httpServiceMock.createStartContract(); + const queryClient = new QueryClient(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls getScheduledList with correct arguments', async () => { + (getScheduledReportsList as jest.Mock).mockResolvedValueOnce({ data: [] }); + + const { result } = renderHook(() => useGetScheduledList({ http, index: 1, size: 10 }), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual({ data: [] }); + }); + + expect(getScheduledReportsList).toBeCalledWith({ + http, + index: 1, + size: 10, + }); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx similarity index 75% rename from x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts rename to x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx index 5156546f5b153..34cde02bcb0c8 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list_query.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx @@ -14,13 +14,15 @@ export const getKey = queryKeys.getScheduledList; interface GetScheduledListQueryProps { http: HttpSetup; - page?: number; - perPage?: number; + index?: number; + size?: number; } -export const useGetScheduledListQuery = (props: GetScheduledListQueryProps) => { +export const useGetScheduledList = (props: GetScheduledListQueryProps) => { + const { index = 1, size = 10 } = props; return useQuery({ - queryKey: getKey(), + queryKey: getKey({ index, size }), queryFn: () => getScheduledReportsList(props), + keepPreviousData: true, }); }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/index.ts b/x-pack/platform/plugins/private/reporting/public/management/index.ts index 1f5fce0f9efa0..9bc608a4a8fb6 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/index.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/index.ts @@ -24,4 +24,4 @@ export type ListingPropsInternal = ListingProps & { http: HttpSetup; }; -export { ReportListing } from './report_listing'; +export { ReportingTabs } from './components/reporting_tabs'; diff --git a/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts b/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts index 4f1acf3ffea0f..8a99d4fe0e638 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/query_keys.ts @@ -7,7 +7,7 @@ const root = 'reporting'; export const queryKeys = { - getScheduledList: () => [root, 'scheduledList'], + getScheduledList: (params: unknown) => [root, 'scheduledList', params] as const, getHealth: () => [root, 'health'] as const, }; From 368e4bc898616e93264a17889cdff28c4e1aea7c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 17 Jun 2025 12:36:19 +0100 Subject: [PATCH 04/15] add more unit tests, update ilm links --- .../private/reporting/common/test/fixtures.ts | 50 +++++ .../apis/get_scheduled_reports_list.ts | 14 +- .../management/components/ilm_policy_link.tsx | 5 +- .../components/report_diagnostic.tsx | 12 +- .../components/report_exports_table.test.tsx | 106 ++++++++++ .../components/report_exports_table.tsx | 21 +- .../components/report_schedule_indicator.tsx | 2 +- .../report_schedules_table.test.tsx | 198 ++++++++++++++++++ .../components/report_schedules_table.tsx | 73 ++++--- .../management/components/reporting_tabs.tsx | 8 +- .../private/reporting/public/translations.ts | 14 ++ 11 files changed, 429 insertions(+), 74 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.test.tsx create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx diff --git a/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts b/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts index 823785c4eb273..8c492e77e1c93 100644 --- a/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts +++ b/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts @@ -5,8 +5,10 @@ * 2.0. */ +import { Frequency } from '@kbn/rrule'; import { JOB_STATUS } from '@kbn/reporting-common'; import { ReportApiJSON } from '@kbn/reporting-common/types'; +import { ListScheduledReportApiJSON } from '../../server/types'; import type { ReportMock } from './types'; const buildMockReport = (baseObj: ReportMock): ReportApiJSON => ({ @@ -173,3 +175,51 @@ export const mockJobs: ReportApiJSON[] = [ status: JOB_STATUS.COMPLETED, }), ]; + +export const mockScheduledReports: ListScheduledReportApiJSON[] = [ + { + created_at: '2025-06-10T12:41:45.136Z', + created_by: 'Foo Bar', + enabled: true, + id: 'scheduled-report-1', + jobtype: 'printable_pdf_v2', + last_run: '2025-05-10T12:41:46.959Z', + next_run: '2025-06-16T13:56:07.123Z', + object_type: 'dashboard', + schedule: { + rrule: { freq: Frequency.WEEKLY, tzid: 'UTC', interval: 1 }, + }, + title: 'Scheduled report 1', + notification: {}, + }, + { + created_at: '2025-06-16T12:41:45.136Z', + created_by: 'Test abc', + enabled: true, + id: 'scheduled-report-2', + jobtype: 'printable_pdf_v2', + last_run: '2025-06-16T12:41:46.959Z', + next_run: '2025-06-16T13:56:07.123Z', + object_type: 'discover', + schedule: { + rrule: { freq: Frequency.DAILY, tzid: 'UTC', interval: 1 }, + }, + title: 'Scheduled report 2', + notification: {}, + }, + { + created_at: '2025-06-12T12:41:45.136Z', + created_by: 'New', + enabled: false, + id: 'scheduled-report-3', + jobtype: 'printable_pdf_v2', + last_run: '2025-06-16T12:41:46.959Z', + next_run: '2025-06-16T13:56:07.123Z', + object_type: 'discover', + schedule: { + rrule: { freq: Frequency.MONTHLY, tzid: 'UTC', interval: 2 }, + }, + title: 'Scheduled report 3', + notification: {}, + }, +]; diff --git a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts index 3fdabcc90d50b..d6152936ff554 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts @@ -7,7 +7,6 @@ import { HttpFetchQuery, HttpSetup } from '@kbn/core/public'; import { INTERNAL_ROUTES } from '@kbn/reporting-common'; -import { ScheduledReport } from '@kbn/reporting-common/types'; import { type ListScheduledReportApiJSON } from '../../../server/types'; export interface Pagination { @@ -27,7 +26,7 @@ export const getScheduledReportsList = async ({ page: number; size: number; total: number; - data: ScheduledReport[]; + data: ListScheduledReportApiJSON[]; }> => { const query: HttpFetchQuery = { page: index, size }; @@ -40,19 +39,10 @@ export const getScheduledReportsList = async ({ query, }); - const transformedData: ScheduledReport[] = res.data.map((item) => ({ - ...item, - createdAt: item.created_at, - createdBy: item.created_by, - lastRun: item.last_run, - nextRun: item.next_run, - objectType: item.object_type, - })); - return { page: res.page, size: res.per_page, total: res.total, - data: transformedData, + data: res.data, }; }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx index 8bb72cddd6c76..bb5c3a572e17e 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx @@ -22,7 +22,7 @@ interface Props { const i18nTexts = { buttonLabel: i18n.translate('xpack.reporting.listing.reports.ilmPolicyLinkText', { - defaultMessage: 'Edit reporting ILM policy', + defaultMessage: 'Edit ILM policy', }), }; @@ -30,7 +30,8 @@ export const IlmPolicyLink: FunctionComponent = ({ locator, navigateToUrl return ( { const url = locator.getRedirectUrl({ page: 'policy_edit', diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_diagnostic.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_diagnostic.tsx index 90139a56ead28..c6ea3e874e322 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_diagnostic.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_diagnostic.tsx @@ -10,7 +10,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, - EuiButtonEmpty, EuiCallOut, EuiFlyout, EuiFlyoutBody, @@ -182,17 +181,12 @@ export const ReportDiagnostic = ({ apiClient, clientConfig }: Props) => { {configAllowsImageReports && (
{flyout} - + - +
)} diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.test.tsx new file mode 100644 index 0000000000000..7d381f3921957 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { + applicationServiceMock, + coreMock, + httpServiceMock, + notificationServiceMock, +} from '@kbn/core/public/mocks'; +import { ReportExportsTable } from './report_exports_table'; +import { render, screen } from '@testing-library/react'; +import { Job, ReportingAPIClient } from '@kbn/reporting-public'; +import { Observable } from 'rxjs'; +import { ILicense } from '@kbn/licensing-plugin/public'; +import { SharePluginSetup } from '@kbn/share-plugin/public'; +import { mockConfig } from '../__test__/report_listing.test.helpers'; +import React from 'react'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '@kbn/reporting-common'; +import { mockJobs } from '../../../common/test'; +import { RecursivePartial, UseEuiTheme } from '@elastic/eui'; +import { ThemeProvider } from '@emotion/react'; + +const coreStart = coreMock.createStart(); +const http = httpServiceMock.createSetupContract(); +const uiSettingsClient = coreMock.createSetup().uiSettings; +const httpService = httpServiceMock.createSetupContract(); +const application = applicationServiceMock.createStartContract(); +const reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); +const validCheck = { + check: () => ({ + state: 'VALID', + message: '', + }), +}; +const license$ = { + subscribe: (handler: unknown) => { + return (handler as Function)(validCheck); + }, +} as Observable; + +export const getMockTheme = (partialTheme: RecursivePartial): UseEuiTheme => + partialTheme as UseEuiTheme; + +const defaultProps = { + coreStart, + http, + application, + apiClient: reportingAPIClient, + config: mockConfig, + license$, + urlService: {} as unknown as SharePluginSetup['url'], + toasts: notificationServiceMock.createSetupContract().toasts, + capabilities: application.capabilities, + redirect: application.navigateToApp, + navigateToUrl: application.navigateToUrl, +}; + +describe('ReportExportsTable', () => { + const mockTheme = getMockTheme({ euiTheme: { size: { s: '' } } }); + beforeEach(() => { + jest.clearAllMocks(); + jest + .spyOn(reportingAPIClient, 'list') + .mockImplementation(() => Promise.resolve(mockJobs.map((j) => new Job(j)))); + jest.spyOn(reportingAPIClient, 'total').mockImplementation(() => Promise.resolve(18)); + }); + + it('renders table correctly', async () => { + render( + + + + ); + + expect(await screen.findByTestId(REPORT_TABLE_ID)).toBeInTheDocument(); + }); + + it('renders empty state correctly', async () => { + jest.spyOn(reportingAPIClient, 'list').mockImplementation(() => Promise.resolve([])); + jest.spyOn(reportingAPIClient, 'total').mockImplementation(() => Promise.resolve(0)); + render( + + + + ); + + expect(await screen.findByText('No reports have been created')).toBeInTheDocument(); + }); + + it('renders data correctly', async () => { + render( + + + + ); + + expect(await screen.findAllByTestId(REPORT_TABLE_ROW_ID)).toHaveLength(mockJobs.length); + + expect(await screen.findByTestId(`viewReportingLink-${mockJobs[0].id}`)).toBeInTheDocument(); + expect(await screen.findByTestId(`reportDownloadLink-${mockJobs[0].id}`)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 58003939e7f0e..9ea704d92bb48 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -29,6 +29,7 @@ import { prettyPrintJobType } from '../../../common/job_utils'; import { Poller } from '../../../common/poller'; import { ReportDeleteButton, ReportInfoFlyout, ReportStatusIndicator } from '.'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; +import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; type TableColumn = EuiBasicTableColumn; @@ -318,7 +319,9 @@ export class ReportExportsTable extends Component { name: i18n.translate('xpack.reporting.exports.tableColumns.content', { defaultMessage: 'Content', }), - render: (_status: string, job) => prettyPrintJobType(job.jobtype), + render: (_status: string, job) => ( +
{prettyPrintJobType(job.jobtype)}
+ ), mobileOptions: { show: false, }, @@ -329,9 +332,9 @@ export class ReportExportsTable extends Component { name: i18n.translate('xpack.reporting.exports.tableColumns.exportType', { defaultMessage: 'Export type', }), - render: (_scheduledReportId: string, job) => { - return job.getExportType(); - }, + render: (_scheduledReportId: string, job) => ( +
{job.getExportType()}
+ ), mobileOptions: { show: false, }, @@ -428,15 +431,7 @@ export class ReportExportsTable extends Component { items={this.state.jobs} loading={this.state.isLoading} columns={tableColumns} - noItemsMessage={ - this.state.isLoading - ? i18n.translate('xpack.reporting.exports.table.loadingReportsDescription', { - defaultMessage: 'Loading reports', - }) - : i18n.translate('xpack.reporting.exports.table.noCreatedReportsDescription', { - defaultMessage: 'No reports have been created', - }) - } + noItemsMessage={NO_CREATED_REPORTS_DESCRIPTION} pagination={pagination} selection={selection} onChange={this.onTableChange} diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx index 040e3df30880a..bcdf1920337bf 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx @@ -47,7 +47,7 @@ export const ReportScheduleIndicator: FC = ({ sche return ( diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx new file mode 100644 index 0000000000000..5cbbda7732e80 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx @@ -0,0 +1,198 @@ +/* + * 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 { + applicationServiceMock, + coreMock, + httpServiceMock, + notificationServiceMock, +} from '@kbn/core/public/mocks'; +import { render, screen, waitFor } from '@testing-library/react'; +import { ReportingAPIClient } from '@kbn/reporting-public'; +import { Observable } from 'rxjs'; +import { ILicense } from '@kbn/licensing-plugin/public'; +import { SharePluginSetup } from '@kbn/share-plugin/public'; +import { mockConfig } from '../__test__/report_listing.test.helpers'; +import React from 'react'; +import { RecursivePartial, UseEuiTheme } from '@elastic/eui'; +import ReportSchedulesTable from './report_schedules_table'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { useGetScheduledList } from '../hooks/use_get_scheduled_list'; +import { mockScheduledReports } from '../../../common/test/fixtures'; +import { userEvent } from '@testing-library/user-event'; +import { useBulkDisable } from '../hooks/use_bulk_disable'; + +jest.mock('../hooks/use_get_scheduled_list', () => ({ + useGetScheduledList: jest.fn(), +})); +jest.mock('../hooks/use_bulk_disable'); + +const useBulkDisableMock = useBulkDisable as jest.Mock; + +const coreStart = coreMock.createStart(); +const http = httpServiceMock.createSetupContract(); +const uiSettingsClient = coreMock.createSetup().uiSettings; +const httpService = httpServiceMock.createSetupContract(); +const application = applicationServiceMock.createStartContract(); +const reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); +const validCheck = { + check: () => ({ + state: 'VALID', + message: '', + }), +}; +const license$ = { + subscribe: (handler: unknown) => { + return (handler as Function)(validCheck); + }, +} as Observable; + +export const getMockTheme = (partialTheme: RecursivePartial): UseEuiTheme => + partialTheme as UseEuiTheme; + +const defaultProps = { + coreStart, + http, + application, + apiClient: reportingAPIClient, + config: mockConfig, + license$, + urlService: {} as unknown as SharePluginSetup['url'], + toasts: notificationServiceMock.createSetupContract().toasts, + capabilities: application.capabilities, + redirect: application.navigateToApp, + navigateToUrl: application.navigateToUrl, +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}); + +describe('ReportSchedulesTable', () => { + const bulkDisableScheduledReportsMock = jest.fn(); + useBulkDisableMock.mockReturnValue({ + isLoading: false, + mutateAsync: bulkDisableScheduledReportsMock, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders table correctly', async () => { + (useGetScheduledList as jest.Mock).mockReturnValueOnce({ + data: { + page: 0, + size: 10, + total: 0, + data: [], + }, + isLoading: false, + }); + + render( + + + + + + ); + + expect(await screen.findByTestId('reportSchedulesTable')).toBeInTheDocument(); + }); + + it('renders empty state correctly', async () => { + (useGetScheduledList as jest.Mock).mockReturnValueOnce({ + data: { + page: 0, + size: 10, + total: 0, + data: [], + }, + isLoading: false, + }); + + render( + + + + + + ); + + expect(await screen.findByText('No reports have been created')).toBeInTheDocument(); + }); + + it('renders data correctly', async () => { + (useGetScheduledList as jest.Mock).mockReturnValueOnce({ + data: { + page: 3, + size: 10, + total: 3, + data: mockScheduledReports, + }, + isLoading: false, + }); + + render( + + + + + + ); + + expect(await screen.findAllByTestId('scheduledReportRow')).toHaveLength(3); + expect(await screen.findByText(mockScheduledReports[0].title)).toBeInTheDocument(); + expect(await screen.findAllByText('Active')).toHaveLength(2); + expect(await screen.findAllByText('Disabled')).toHaveLength(1); + }); + + it('disable schedule report correctly', async () => { + (useGetScheduledList as jest.Mock).mockReturnValueOnce({ + data: { + page: 3, + size: 10, + total: 3, + data: mockScheduledReports, + }, + isLoading: false, + }); + + render( + + + + + + ); + + expect(await screen.findAllByTestId('scheduledReportRow')).toHaveLength(3); + + userEvent.click((await screen.findAllByTestId('euiCollapsedItemActionsButton'))[0]); + + const firstReportDisable = await screen.findByTestId( + `reportDisableSchedule-${mockScheduledReports[0].id}` + ); + + expect(firstReportDisable).toBeInTheDocument(); + + userEvent.click(firstReportDisable); + + await waitFor(() => { + expect(bulkDisableScheduledReportsMock).toHaveBeenCalledWith({ + ids: [mockScheduledReports[0].id], + }); + }); + }); +}); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index e1b606ac1b56c..526fe1ba7a610 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -20,14 +20,17 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ScheduledReport } from '@kbn/reporting-common/types'; import moment from 'moment'; +import { stringify } from 'query-string'; +import { REPORTING_REDIRECT_APP, buildKibanaPath } from '@kbn/reporting-common'; +import { type ListScheduledReportApiJSON } from '../../../server/types'; import { ListingPropsInternal } from '..'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; import { useGetScheduledList } from '../hooks/use_get_scheduled_list'; import { prettyPrintJobType } from '../../../common/job_utils'; import { ReportScheduleIndicator } from './report_schedule_indicator'; import { useBulkDisable } from '../hooks/use_bulk_disable'; +import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; interface QueryParams { index: number; @@ -36,6 +39,7 @@ interface QueryParams { export const ReportSchedulesTable = (props: ListingPropsInternal) => { const { http, toasts } = props; + const [queryParams, setQueryParams] = useState({ index: 1, size: 10, @@ -48,22 +52,21 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { const { mutateAsync: bulkDisableScheduledReports } = useBulkDisable({ http, toasts, - ...queryParams, }); - const tableColumns: Array> = [ + const tableColumns: Array> = [ { - field: 'objectType', + field: 'object_type', name: i18n.translate('xpack.reporting.schedules.tableColumns.typeTitle', { defaultMessage: 'Type', }), width: '5%', - render: (objectType: string) => ( + render: (_objectType: string) => ( ), }, @@ -73,9 +76,9 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Title', }), width: '20%', - render: (title: string, item: ScheduledReport) => ( + render: (_title: string, item: ListScheduledReportApiJSON) => ( {}}> - {title} + {_title} ), mobileOptions: { @@ -89,7 +92,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Status', }), width: '10%', - render: (status: string, item: ScheduledReport) => { + render: (_status: string, item: ListScheduledReportApiJSON) => { return ( { defaultMessage: 'Schedule', }), width: '15%', - render: (schedule: ScheduledReport['schedule']) => ( - + render: (_schedule: ListScheduledReportApiJSON['schedule']) => ( + ), }, { - field: 'nextRun', + field: 'next_run', name: i18n.translate('xpack.reporting.schedules.tableColumns.nextScheduleTitle', { defaultMessage: 'Next schedule', }), width: '20%', - render: (nextRun: string) => { - return moment(nextRun).format('YYYY-MM-DD @ hh:mm A'); + render: (_nextRun: string) => { + return moment(_nextRun).format('YYYY-MM-DD @ hh:mm A'); }, }, { @@ -132,18 +135,18 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { name: i18n.translate('xpack.reporting.schedules.tableColumns.fileType', { defaultMessage: 'File Type', }), - render: (jobtype: string) => prettyPrintJobType(jobtype), + render: (_jobtype: string) => prettyPrintJobType(_jobtype), mobileOptions: { show: false, }, }, { - field: 'createdBy', + field: 'created_by', name: i18n.translate('xpack.reporting.schedules.tableColumns.createdByTitle', { defaultMessage: 'Created by', }), width: '15%', - render: (createdBy: string) => { + render: (_createdBy: string) => { return ( { responsive={false} > - + - {createdBy} + {_createdBy} @@ -177,7 +180,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { description: i18n.translate('xpack.reporting.schedules.table.viewConfig.description', { defaultMessage: 'View schedule configuration details', }), - 'data-test-subj': 'reportViewConfig', + 'data-test-subj': (item) => `reportViewConfig-${item.id}`, type: 'icon', icon: 'calendar', onClick: () => {}, @@ -189,20 +192,22 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { description: i18n.translate('xpack.reporting.schedules.table.openDashboard.description', { defaultMessage: 'Open associated dashboard', }), - 'data-test-subj': 'reportOpenDashboard', + 'data-test-subj': (item) => `reportOpenDashboard-${item.id}`, type: 'icon', icon: 'dashboardApp', - + // available: (job) => job.canLinkToKibanaApp, onClick: (item) => { - // const searchParams = stringify({ id: item.id }); - // const path = buildKibanaPath({ - // basePath: http.basePath.serverBasePath, - // // spaceId: .spaceId, - // appPath: REPORTING_REDIRECT_APP, - // }); - // const href = `${path}?${searchParams}`; - // window.open(href, '_blank'); - // window.focus(); + const searchParams = stringify({ id: item.id }); + + const path = buildKibanaPath({ + basePath: http.basePath.serverBasePath, + // spaceId: job.spaceId, + appPath: REPORTING_REDIRECT_APP, + }); + + const href = `${path}?${searchParams}`; + window.open(href, '_blank'); + window.focus(); }, }, { @@ -215,7 +220,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Disable report schedule', } ), - 'data-test-subj': 'reportDisableSchedule', + 'data-test-subj': (item) => `reportDisableSchedule-${item.id}`, enabled: (item) => item.enabled, type: 'icon', icon: 'cross', @@ -251,7 +256,9 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { pageSize: queryParams.size, totalItemCount: scheduledList?.total ?? 0, }} + noItemsMessage={NO_CREATED_REPORTS_DESCRIPTION} onChange={tableOnChangeCallback} + rowProps={() => ({ 'data-test-subj': 'scheduledReportRow' })} />
); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx index 35afb30521dc4..b18ed60cc8013 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -149,6 +149,9 @@ export const ReportingTabs: React.FunctionComponent< config.statefulSettings.enabled ? [ , + + + , {capabilities?.management?.data?.index_lifecycle_management && ( @@ -160,11 +163,8 @@ export const ReportingTabs: React.FunctionComponent< ) )} - )}{' '} + )} , - - - , ] : [] } diff --git a/x-pack/platform/plugins/private/reporting/public/translations.ts b/x-pack/platform/plugins/private/reporting/public/translations.ts index 878a3b28d722b..b5fd9977f8a5c 100644 --- a/x-pack/platform/plugins/private/reporting/public/translations.ts +++ b/x-pack/platform/plugins/private/reporting/public/translations.ts @@ -14,3 +14,17 @@ export const APP_TITLE = i18n.translate('xpack.reporting.registerFeature.reporti export const APP_DESC = i18n.translate('xpack.reporting.registerFeature.reportingDescription', { defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', }); + +export const LOADING_REPORTS_DESCRIPTION = i18n.translate( + 'xpack.reporting.table.loadingReportsDescription', + { + defaultMessage: 'Loading reports', + } +); + +export const NO_CREATED_REPORTS_DESCRIPTION = i18n.translate( + 'xpack.reporting.table.noCreatedReportsDescription', + { + defaultMessage: 'No reports have been created', + } +); From 2f187164fbc50cb291416e4d57406cd0062d9168 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 17 Jun 2025 14:13:28 +0100 Subject: [PATCH 05/15] lint, type, translations fix --- .../private/kbn-reporting/common/types.ts | 1 - .../__test__/report_listing.test.helpers.tsx | 28 ++++++++++++++++--- .../translations/translations/fr-FR.json | 21 -------------- .../translations/translations/ja-JP.json | 21 -------------- .../translations/translations/zh-CN.json | 21 -------------- 5 files changed, 24 insertions(+), 68 deletions(-) diff --git a/src/platform/packages/private/kbn-reporting/common/types.ts b/src/platform/packages/private/kbn-reporting/common/types.ts index 68492ec13868d..b9778b11b3659 100644 --- a/src/platform/packages/private/kbn-reporting/common/types.ts +++ b/src/platform/packages/private/kbn-reporting/common/types.ts @@ -14,7 +14,6 @@ import type { import type { ConcreteTaskInstance, RruleSchedule } from '@kbn/task-manager-plugin/server'; import { JOB_STATUS } from './constants'; import type { LocatorParams } from './url'; -import { Rrule } from '@kbn/task-manager-plugin/server/task'; export * from './url'; diff --git a/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx b/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx index 793b49979cb6f..883a1f19b637c 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx @@ -31,10 +31,13 @@ import { act } from 'react-dom/test-utils'; import { Observable } from 'rxjs'; import { EuiThemeProvider } from '@elastic/eui'; -import { ListingProps as Props, ReportListing } from '..'; +import { RouteComponentProps } from 'react-router-dom'; +import { createLocation, createMemoryHistory } from 'history'; +import { ListingProps as Props, ReportingTabs } from '..'; import { mockJobs } from '../../../common/test'; import { IlmPolicyStatusContextProvider } from '../../lib/ilm_policy_status_context'; import { ReportDiagnostic } from '../components'; +import { MatchParams } from '../components/reporting_tabs'; export interface TestDependencies { http: ReturnType; @@ -90,6 +93,21 @@ const license$ = { }, } as Observable; +const routeProps: RouteComponentProps = { + history: createMemoryHistory({ + initialEntries: ['/exports'], + }), + location: createLocation('/exports'), + match: { + isExact: true, + path: `/exports`, + url: '', + params: { + section: 'exports', + }, + }, +}; + export const createTestBed = registerTestBed( ({ http, @@ -107,14 +125,16 @@ export const createTestBed = registerTestBed( - diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index c8f101da9f285..884ea4bb0de41 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -33379,41 +33379,20 @@ "xpack.reporting.listing.infoPanel.tzInfo": "Fuseau horaire", "xpack.reporting.listing.infoPanel.unknownLabel": "inconnue", "xpack.reporting.listing.reports.ilmPolicyLinkText": "Modifier la politique ILM de reporting", - "xpack.reporting.listing.reports.subtitle": "Obtenir les rapports générés dans les applications Kibana.", "xpack.reporting.listing.reports.subtitleStateful": "Obtenir les rapports générés dans les applications Kibana.", "xpack.reporting.listing.reports.titleStateful": "Rapports", - "xpack.reporting.listing.reportstitle": "Rapports", - "xpack.reporting.listing.table.captionDescription": "Rapports générés dans les applications Kibana", "xpack.reporting.listing.table.deleteCancelButton": "Annuler", - "xpack.reporting.listing.table.deleteConfim": "Le rapport {reportTitle} a été supprimé", "xpack.reporting.listing.table.deleteConfirmButton": "Supprimer", "xpack.reporting.listing.table.deleteConfirmMessage": "Vous ne pouvez pas récupérer les rapports supprimés.", "xpack.reporting.listing.table.deleteConfirmTitle": "Supprimer le rapport \"{name}\" ?", - "xpack.reporting.listing.table.deleteFailedErrorMessage": "Le rapport n'a pas été supprimé : {error}", "xpack.reporting.listing.table.deleteNumConfirmTitle": "Supprimer les {num} rapports sélectionnés ?", "xpack.reporting.listing.table.deleteReportButton": "Supprimer {num, plural, one {le rapport} other {les rapports} }", - "xpack.reporting.listing.table.downloadReportButtonLabel": "Télécharger le rapport", - "xpack.reporting.listing.table.downloadReportDescription": "Téléchargez ce rapport dans un nouvel onglet.", - "xpack.reporting.listing.table.loadingReportsDescription": "Chargement des rapports", - "xpack.reporting.listing.table.noCreatedReportsDescription": "Aucun rapport n'a été créé", - "xpack.reporting.listing.table.noTitleLabel": "Sans titre", - "xpack.reporting.listing.table.openInKibanaAppDescription": "Ouvrez l’application Kibana directement à l’emplacement de génération de ce rapport.", - "xpack.reporting.listing.table.openInKibanaAppLabel": "Ouvrir dans Kibana", "xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip": "Consultez les informations de rapport et le message d'erreur.", "xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip": "Consultez les informations de rapport et les avertissements.", "xpack.reporting.listing.table.reportInfoButtonTooltip": "Consultez les informations de rapport.", "xpack.reporting.listing.table.reportInfoUnableToFetch": "Impossible de récupérer les informations de rapport.", - "xpack.reporting.listing.table.requestFailedErrorMessage": "Demande refusée", "xpack.reporting.listing.table.showReportInfoAriaLabel": "Afficher les informations de rapport", "xpack.reporting.listing.table.untitledReport": "Rapport sans titre", - "xpack.reporting.listing.table.viewReportingInfoActionButtonDescription": "Accédez à des informations supplémentaires sur ce rapport.", - "xpack.reporting.listing.table.viewReportingInfoActionButtonLabel": "Afficher les informations du rapport", - "xpack.reporting.listing.tableColumns.actionsTitle": "Actions", - "xpack.reporting.listing.tableColumns.content": "Contenu", - "xpack.reporting.listing.tableColumns.createdAtTitle": "Créé à", - "xpack.reporting.listing.tableColumns.reportTitle": "Titre", - "xpack.reporting.listing.tableColumns.statusTitle": "Statut", - "xpack.reporting.listing.tableColumns.typeTitle": "Type", "xpack.reporting.management.reportingTitle": "Reporting", "xpack.reporting.pdfFooterImageDescription": "Image personnalisée à utiliser dans le pied de page du PDF", "xpack.reporting.pdfFooterImageLabel": "Image de pied de page du PDF", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 4a6b959681a3c..01b29ecc158bd 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -33356,41 +33356,20 @@ "xpack.reporting.listing.infoPanel.tzInfo": "タイムゾーン", "xpack.reporting.listing.infoPanel.unknownLabel": "不明", "xpack.reporting.listing.reports.ilmPolicyLinkText": "レポートILMポリシーを編集", - "xpack.reporting.listing.reports.subtitle": "Kibanaアプリケーションで生成されたレポートを取得します。", "xpack.reporting.listing.reports.subtitleStateful": "Kibanaアプリケーションで生成されたレポートを取得します。", "xpack.reporting.listing.reports.titleStateful": "レポート", - "xpack.reporting.listing.reportstitle": "レポート", - "xpack.reporting.listing.table.captionDescription": "Kibanaアプリケーションでレポートが生成されました", "xpack.reporting.listing.table.deleteCancelButton": "キャンセル", - "xpack.reporting.listing.table.deleteConfim": "{reportTitle} レポートを削除しました", "xpack.reporting.listing.table.deleteConfirmButton": "削除", "xpack.reporting.listing.table.deleteConfirmMessage": "削除されたレポートは復元できません。", "xpack.reporting.listing.table.deleteConfirmTitle": "「{name}」レポートを削除しますか?", - "xpack.reporting.listing.table.deleteFailedErrorMessage": "レポートは削除されませんでした:{error}", "xpack.reporting.listing.table.deleteNumConfirmTitle": "{num}件の選択したレポートを削除しますか?", "xpack.reporting.listing.table.deleteReportButton": "{num, plural, other {件のレポート} }を削除", - "xpack.reporting.listing.table.downloadReportButtonLabel": "レポートをダウンロード", - "xpack.reporting.listing.table.downloadReportDescription": "このレポートを新しいタブでダウンロードします。", - "xpack.reporting.listing.table.loadingReportsDescription": "レポートを読み込み中です", - "xpack.reporting.listing.table.noCreatedReportsDescription": "レポートが作成されていません", - "xpack.reporting.listing.table.noTitleLabel": "無題", - "xpack.reporting.listing.table.openInKibanaAppDescription": "このレポートが生成されたKibanaアプリを開きます。", - "xpack.reporting.listing.table.openInKibanaAppLabel": "Kibanaで開く", "xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip": "レポート情報とエラーメッセージを参照してください。", "xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip": "レポート情報と警告を参照してください。", "xpack.reporting.listing.table.reportInfoButtonTooltip": "レポート情報を参照してください。", "xpack.reporting.listing.table.reportInfoUnableToFetch": "レポート情報を取得できません。", - "xpack.reporting.listing.table.requestFailedErrorMessage": "リクエストに失敗しました", "xpack.reporting.listing.table.showReportInfoAriaLabel": "レポート情報を表示", "xpack.reporting.listing.table.untitledReport": "無題のレポート", - "xpack.reporting.listing.table.viewReportingInfoActionButtonDescription": "このレポートの詳細を表示してください。", - "xpack.reporting.listing.table.viewReportingInfoActionButtonLabel": "レポート情報を表示", - "xpack.reporting.listing.tableColumns.actionsTitle": "アクション", - "xpack.reporting.listing.tableColumns.content": "コンテンツ", - "xpack.reporting.listing.tableColumns.createdAtTitle": "作成日時:", - "xpack.reporting.listing.tableColumns.reportTitle": "タイトル", - "xpack.reporting.listing.tableColumns.statusTitle": "ステータス", - "xpack.reporting.listing.tableColumns.typeTitle": "型", "xpack.reporting.management.reportingTitle": "レポート", "xpack.reporting.pdfFooterImageDescription": "PDFのフッターに使用するカスタム画像です", "xpack.reporting.pdfFooterImageLabel": "PDFフッター画像", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index fef186f203536..ce4449fbf6856 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -33413,41 +33413,20 @@ "xpack.reporting.listing.infoPanel.tzInfo": "时区", "xpack.reporting.listing.infoPanel.unknownLabel": "未知", "xpack.reporting.listing.reports.ilmPolicyLinkText": "编辑报告 ILM 策略", - "xpack.reporting.listing.reports.subtitle": "获取在 Kibana 应用程序中生成的报告。", "xpack.reporting.listing.reports.subtitleStateful": "获取在 Kibana 应用程序中生成的报告。", "xpack.reporting.listing.reports.titleStateful": "报告", - "xpack.reporting.listing.reportstitle": "报告", - "xpack.reporting.listing.table.captionDescription": "在 Kibana 应用程序中生成的报告", "xpack.reporting.listing.table.deleteCancelButton": "取消", - "xpack.reporting.listing.table.deleteConfim": "报告 {reportTitle} 已删除", "xpack.reporting.listing.table.deleteConfirmButton": "删除", "xpack.reporting.listing.table.deleteConfirmMessage": "您无法恢复删除的报告。", "xpack.reporting.listing.table.deleteConfirmTitle": "删除“{name}”报告?", - "xpack.reporting.listing.table.deleteFailedErrorMessage": "报告未删除:{error}", "xpack.reporting.listing.table.deleteNumConfirmTitle": "删除 {num} 个选定报告?", "xpack.reporting.listing.table.deleteReportButton": "删除{num, plural, other {报告} }", - "xpack.reporting.listing.table.downloadReportButtonLabel": "下载报告", - "xpack.reporting.listing.table.downloadReportDescription": "在新选项卡中下载此报告。", - "xpack.reporting.listing.table.loadingReportsDescription": "正在载入报告", - "xpack.reporting.listing.table.noCreatedReportsDescription": "未创建任何报告", - "xpack.reporting.listing.table.noTitleLabel": "未命名", - "xpack.reporting.listing.table.openInKibanaAppDescription": "打开生成此报告的 Kibana 应用。", - "xpack.reporting.listing.table.openInKibanaAppLabel": "在 Kibana 中打开", "xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip": "查看报告信息和错误消息。", "xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip": "查看报告信息和警告。", "xpack.reporting.listing.table.reportInfoButtonTooltip": "查看报告信息。", "xpack.reporting.listing.table.reportInfoUnableToFetch": "无法提取报告信息。", - "xpack.reporting.listing.table.requestFailedErrorMessage": "请求失败", "xpack.reporting.listing.table.showReportInfoAriaLabel": "显示报告信息", "xpack.reporting.listing.table.untitledReport": "未命名报告", - "xpack.reporting.listing.table.viewReportingInfoActionButtonDescription": "查看有关此报告的其他信息。", - "xpack.reporting.listing.table.viewReportingInfoActionButtonLabel": "查看报告信息", - "xpack.reporting.listing.tableColumns.actionsTitle": "操作", - "xpack.reporting.listing.tableColumns.content": "内容", - "xpack.reporting.listing.tableColumns.createdAtTitle": "创建于", - "xpack.reporting.listing.tableColumns.reportTitle": "标题", - "xpack.reporting.listing.tableColumns.statusTitle": "状态", - "xpack.reporting.listing.tableColumns.typeTitle": "类型", "xpack.reporting.management.reportingTitle": "Reporting", "xpack.reporting.pdfFooterImageDescription": "要在 PDF 的页脚中使用的定制图像", "xpack.reporting.pdfFooterImageLabel": "PDF 页脚图像", From de41a2b4384c3d3ffe1f2e8462fc6c87aca642de Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:48:34 +0000 Subject: [PATCH 06/15] [CI] Auto-commit changed files from 'node scripts/notice' --- .../platform/plugins/private/reporting/tsconfig.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/private/reporting/tsconfig.json b/x-pack/platform/plugins/private/reporting/tsconfig.json index 97f9602f4a192..8ac22d028c0ca 100644 --- a/x-pack/platform/plugins/private/reporting/tsconfig.json +++ b/x-pack/platform/plugins/private/reporting/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "target/types", + "outDir": "target/types" }, "include": ["common/**/*", "public/**/*", "server/**/*", "../../../../../typings/**/*"], "kbn_references": [ @@ -58,11 +58,13 @@ "@kbn/notifications-plugin", "@kbn/spaces-utils", "@kbn/logging-mocks", + "@kbn/shared-ux-router", + "@kbn/controls-plugin", + "@kbn/dashboard-plugin", + "@kbn/core-http-browser-mocks", "@kbn/core-http-browser", "@kbn/response-ops-recurring-schedule-form", - "@kbn/core-mount-utils-browser-internal", + "@kbn/core-mount-utils-browser-internal" ], - "exclude": [ - "target/**/*", - ] + "exclude": ["target/**/*"] } From fa65c5e8ac89f57204b643ab4f9e55dfa4067e46 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 18 Jun 2025 14:09:58 +0100 Subject: [PATCH 07/15] add open dashboard feature --- .../private/kbn-reporting/public/job.tsx | 30 +-------------- .../public/reporting_api_client.test.ts | 21 ++++++++++ .../public/reporting_api_client.ts | 22 ++++++++++- .../private/reporting/common/test/fixtures.ts | 38 +++++++++++++++---- .../apis/get_scheduled_reports_list.ts | 6 +-- .../components/report_exports_table.tsx | 26 +++++++++++-- .../components/report_schedule_indicator.tsx | 4 +- .../components/report_schedules_table.tsx | 21 +++++----- .../public/redirect/redirect_app.tsx | 12 ++++-- 9 files changed, 120 insertions(+), 60 deletions(-) diff --git a/src/platform/packages/private/kbn-reporting/public/job.tsx b/src/platform/packages/private/kbn-reporting/public/job.tsx index bb300d574938a..bcb001e06f300 100644 --- a/src/platform/packages/private/kbn-reporting/public/job.tsx +++ b/src/platform/packages/private/kbn-reporting/public/job.tsx @@ -10,14 +10,7 @@ import moment from 'moment'; import React from 'react'; -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiText, - EuiTextColor, -} from '@elastic/eui'; +import { EuiText, EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JOB_STATUS } from '@kbn/reporting-common'; import type { @@ -263,27 +256,6 @@ export class Job { return this.formatDate(this.created_at); } - getExportType(): React.ReactElement { - const exportType = this.scheduled_report_id - ? i18n.translate('reporting.jobExportType.scheduled', { - defaultMessage: 'Scheduled', - }) - : i18n.translate('reporting.jobExportType.single', { - defaultMessage: 'Single', - }); - - return ( - - - - - - {exportType} - - - ); - } - /* * We use `output.warnings` to show the error of a failed report job, * and to show warnings of a job that completed with warnings. diff --git a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts index 3504ed87956d4..c655715a9b9d6 100644 --- a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts +++ b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.test.ts @@ -118,6 +118,27 @@ describe('ReportingAPIClient', () => { }); }); + describe('getScheduledReportInfo', () => { + beforeEach(() => { + httpClient.get.mockResolvedValueOnce({ data: [{ id: '123', title: 'Scheduled Report 1' }] }); + }); + + it('should send a get request', async () => { + await apiClient.getScheduledReportInfo('123'); + + expect(httpClient.get).toHaveBeenCalledWith( + expect.stringContaining('/internal/reporting/scheduled/list') + ); + }); + + it('should return a report', async () => { + await expect(apiClient.getScheduledReportInfo('123')).resolves.toEqual({ + id: '123', + title: 'Scheduled Report 1', + }); + }); + }); + describe('getError', () => { it('should get an error message', async () => { httpClient.get.mockResolvedValueOnce({ diff --git a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts index 66f5daa0c520a..41c7947d6726b 100644 --- a/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts +++ b/src/platform/packages/private/kbn-reporting/public/reporting_api_client.ts @@ -18,7 +18,13 @@ import { buildKibanaPath, REPORTING_REDIRECT_APP, } from '@kbn/reporting-common'; -import { BaseParams, JobId, ManagementLinkFn, ReportApiJSON } from '@kbn/reporting-common/types'; +import { + BaseParams, + JobId, + ManagementLinkFn, + ReportApiJSON, + ScheduledReportApiJSON, +} from '@kbn/reporting-common/types'; import rison from '@kbn/rison'; import moment from 'moment'; import { stringify } from 'query-string'; @@ -83,7 +89,10 @@ export class ReportingAPIClient implements IReportingAPI { } public getKibanaAppHref(job: Job): string { - const searchParams = stringify({ jobId: job.id }); + const searchParams = stringify({ + jobId: job.id, + ...(job.scheduled_report_id ? { scheduledReportId: job.scheduled_report_id } : {}), + }); const path = buildKibanaPath({ basePath: this.http.basePath.serverBasePath, @@ -158,6 +167,15 @@ export class ReportingAPIClient implements IReportingAPI { return new Job(report); } + public async getScheduledReportInfo(id: string) { + const { data: reportList = [] }: { data: ScheduledReportApiJSON[] } = await this.http.get( + `${INTERNAL_ROUTES.SCHEDULED.LIST}` + ); + + const report = reportList.find((item) => item.id === id); + return report; + } + public async findForJobIds(jobIds: JobId[]) { const reports: ReportApiJSON[] = await this.http.fetch(INTERNAL_ROUTES.JOBS.LIST, { query: { page: 0, ids: jobIds.join(',') }, diff --git a/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts b/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts index 8c492e77e1c93..79a9a278a773b 100644 --- a/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts +++ b/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts @@ -7,8 +7,7 @@ import { Frequency } from '@kbn/rrule'; import { JOB_STATUS } from '@kbn/reporting-common'; -import { ReportApiJSON } from '@kbn/reporting-common/types'; -import { ListScheduledReportApiJSON } from '../../server/types'; +import { BaseParamsV2, ReportApiJSON, ScheduledReportApiJSON } from '@kbn/reporting-common/types'; import type { ReportMock } from './types'; const buildMockReport = (baseObj: ReportMock): ReportApiJSON => ({ @@ -176,7 +175,7 @@ export const mockJobs: ReportApiJSON[] = [ }), ]; -export const mockScheduledReports: ListScheduledReportApiJSON[] = [ +export const mockScheduledReports: ScheduledReportApiJSON[] = [ { created_at: '2025-06-10T12:41:45.136Z', created_by: 'Foo Bar', @@ -185,12 +184,37 @@ export const mockScheduledReports: ListScheduledReportApiJSON[] = [ jobtype: 'printable_pdf_v2', last_run: '2025-05-10T12:41:46.959Z', next_run: '2025-06-16T13:56:07.123Z', - object_type: 'dashboard', schedule: { rrule: { freq: Frequency.WEEKLY, tzid: 'UTC', interval: 1 }, }, title: 'Scheduled report 1', - notification: {}, + space_id: 'default', + payload: { + browserTimezone: 'UTC', + title: 'test PDF allowed', + layout: { + id: 'preserve_layout', + }, + objectType: 'dashboard', + version: '7.14.0', + locatorParams: [ + { + id: 'canvas', + version: '7.14.0', + params: { + dashboardId: '7adfa750-4c81-11e8-b3d7-01146121b73d', + preserveSavedFilters: 'true', + timeRange: { + from: 'now-7d', + to: 'now', + }, + useHash: 'false', + viewMode: 'view', + }, + }, + ], + isDeprecated: false, + } as BaseParamsV2, }, { created_at: '2025-06-16T12:41:45.136Z', @@ -200,7 +224,7 @@ export const mockScheduledReports: ListScheduledReportApiJSON[] = [ jobtype: 'printable_pdf_v2', last_run: '2025-06-16T12:41:46.959Z', next_run: '2025-06-16T13:56:07.123Z', - object_type: 'discover', + space_id: 'default', schedule: { rrule: { freq: Frequency.DAILY, tzid: 'UTC', interval: 1 }, }, @@ -215,7 +239,7 @@ export const mockScheduledReports: ListScheduledReportApiJSON[] = [ jobtype: 'printable_pdf_v2', last_run: '2025-06-16T12:41:46.959Z', next_run: '2025-06-16T13:56:07.123Z', - object_type: 'discover', + space_id: 'space-a', schedule: { rrule: { freq: Frequency.MONTHLY, tzid: 'UTC', interval: 2 }, }, diff --git a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts index d6152936ff554..f177f789cc318 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/apis/get_scheduled_reports_list.ts @@ -7,7 +7,7 @@ import { HttpFetchQuery, HttpSetup } from '@kbn/core/public'; import { INTERNAL_ROUTES } from '@kbn/reporting-common'; -import { type ListScheduledReportApiJSON } from '../../../server/types'; +import { type ScheduledReportApiJSON } from '@kbn/reporting-common/types'; export interface Pagination { index: number; @@ -26,7 +26,7 @@ export const getScheduledReportsList = async ({ page: number; size: number; total: number; - data: ListScheduledReportApiJSON[]; + data: ScheduledReportApiJSON[]; }> => { const query: HttpFetchQuery = { page: index, size }; @@ -34,7 +34,7 @@ export const getScheduledReportsList = async ({ page: number; per_page: number; total: number; - data: ListScheduledReportApiJSON[]; + data: ScheduledReportApiJSON[]; }>(INTERNAL_ROUTES.SCHEDULED.LIST, { query, }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 9ea704d92bb48..d9f1ac9b05d87 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -9,6 +9,7 @@ import { Component, Fragment, default as React } from 'react'; import { Subscription } from 'rxjs'; import { + EuiBadge, EuiBasicTable, EuiBasicTableColumn, EuiFlexGroup, @@ -327,14 +328,31 @@ export class ReportExportsTable extends Component { }, }, { - field: 'exportType', + field: 'scheduled_report_id', width: tableColumnWidths.exportType, name: i18n.translate('xpack.reporting.exports.tableColumns.exportType', { defaultMessage: 'Export type', }), - render: (_scheduledReportId: string, job) => ( -
{job.getExportType()}
- ), + render: (_scheduledReportId: string) => { + const exportType = _scheduledReportId + ? i18n.translate('reporting.jobExportType.scheduled', { + defaultMessage: 'Scheduled', + }) + : i18n.translate('reporting.jobExportType.single', { + defaultMessage: 'Single', + }); + + return ( + + + + + + {exportType} + + + ); + }, mobileOptions: { show: false, }, diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx index bcdf1920337bf..e778468599b10 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx @@ -9,10 +9,10 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { Frequency } from '@kbn/rrule'; import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; -import { ScheduledReport } from '@kbn/reporting-common/types'; +import { ScheduledReportApiJSON } from '@kbn/reporting-common/types'; interface ReportScheduleIndicatorProps { - schedule: ScheduledReport['schedule']; + schedule: ScheduledReportApiJSON['schedule']; } export const ReportScheduleIndicator: FC = ({ schedule }) => { diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index 526fe1ba7a610..8913bcd722995 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { stringify } from 'query-string'; import { REPORTING_REDIRECT_APP, buildKibanaPath } from '@kbn/reporting-common'; -import { type ListScheduledReportApiJSON } from '../../../server/types'; +import type { ScheduledReportApiJSON, BaseParamsV2 } from '@kbn/reporting-common/types'; import { ListingPropsInternal } from '..'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; import { useGetScheduledList } from '../hooks/use_get_scheduled_list'; @@ -54,9 +54,9 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { toasts, }); - const tableColumns: Array> = [ + const tableColumns: Array> = [ { - field: 'object_type', + field: 'payload.objectType', name: i18n.translate('xpack.reporting.schedules.tableColumns.typeTitle', { defaultMessage: 'Type', }), @@ -76,7 +76,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Title', }), width: '20%', - render: (_title: string, item: ListScheduledReportApiJSON) => ( + render: (_title: string, item: ScheduledReportApiJSON) => ( {}}> {_title} @@ -92,7 +92,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Status', }), width: '10%', - render: (_status: string, item: ListScheduledReportApiJSON) => { + render: (_status: string, item: ScheduledReportApiJSON) => { return ( { defaultMessage: 'Schedule', }), width: '15%', - render: (_schedule: ListScheduledReportApiJSON['schedule']) => ( + render: (_schedule: ScheduledReportApiJSON['schedule']) => ( ), }, @@ -195,17 +195,18 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { 'data-test-subj': (item) => `reportOpenDashboard-${item.id}`, type: 'icon', icon: 'dashboardApp', - // available: (job) => job.canLinkToKibanaApp, - onClick: (item) => { - const searchParams = stringify({ id: item.id }); + available: (item) => Boolean((item.payload as BaseParamsV2)?.locatorParams), + onClick: async (item) => { + const searchParams = stringify({ scheduledReportId: item.id }); const path = buildKibanaPath({ basePath: http.basePath.serverBasePath, - // spaceId: job.spaceId, + spaceId: item.payload?.spaceId, appPath: REPORTING_REDIRECT_APP, }); const href = `${path}?${searchParams}`; + window.open(href, '_blank'); window.focus(); }, diff --git a/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx b/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx index 34d4dabd63d24..78e1e088ada74 100644 --- a/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/platform/plugins/private/reporting/public/redirect/redirect_app.tsx @@ -18,7 +18,7 @@ import { REPORTING_REDIRECT_LOCATOR_STORE_KEY, REPORTING_REDIRECT_ALLOWED_LOCATOR_TYPES, } from '@kbn/reporting-common'; -import { LocatorParams } from '@kbn/reporting-common/types'; +import { LocatorParams, BaseParamsV2 } from '@kbn/reporting-common/types'; import { ReportingAPIClient } from '@kbn/reporting-public'; import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; @@ -51,9 +51,15 @@ export const RedirectApp: FunctionComponent = ({ apiClient, screenshotMod try { let locatorParams: undefined | LocatorParams; - const { jobId } = parse(window.location.search); + const { jobId, scheduledReportId } = parse(window.location.search); - if (jobId) { + if (scheduledReportId) { + const scheduledReport = await apiClient.getScheduledReportInfo( + scheduledReportId as string + ); + + locatorParams = (scheduledReport?.payload as BaseParamsV2)?.locatorParams?.[0]; + } else if (jobId) { const result = await apiClient.getInfo(jobId as string); locatorParams = result?.locatorParams?.[0]; } else { From 437b929a2d4bfd208423467900fe101cf2eaf95f Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 18 Jun 2025 14:37:47 +0100 Subject: [PATCH 08/15] update translations --- .../public/management/components/report_exports_table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index d9f1ac9b05d87..85ffda4642cad 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -335,10 +335,10 @@ export class ReportExportsTable extends Component { }), render: (_scheduledReportId: string) => { const exportType = _scheduledReportId - ? i18n.translate('reporting.jobExportType.scheduled', { + ? i18n.translate('xpack.reporting.exports.exportType.scheduled', { defaultMessage: 'Scheduled', }) - : i18n.translate('reporting.jobExportType.single', { + : i18n.translate('xpack.reporting.exports.exportType.single', { defaultMessage: 'Single', }); From 55cf054db9db6c6f03564cd922f521088354cfdc Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 19 Jun 2025 10:35:22 +0100 Subject: [PATCH 09/15] add view schedule config table action --- .../private/reporting/common/test/fixtures.ts | 2 - .../management/components/ilm_policy_link.tsx | 7 +- .../components/report_schedules_table.tsx | 97 ++++++++++++++++++- .../management/components/reporting_tabs.tsx | 4 +- 4 files changed, 97 insertions(+), 13 deletions(-) diff --git a/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts b/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts index 79a9a278a773b..1932582a0b3d9 100644 --- a/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts +++ b/x-pack/platform/plugins/private/reporting/common/test/fixtures.ts @@ -229,7 +229,6 @@ export const mockScheduledReports: ScheduledReportApiJSON[] = [ rrule: { freq: Frequency.DAILY, tzid: 'UTC', interval: 1 }, }, title: 'Scheduled report 2', - notification: {}, }, { created_at: '2025-06-12T12:41:45.136Z', @@ -244,6 +243,5 @@ export const mockScheduledReports: ScheduledReportApiJSON[] = [ rrule: { freq: Frequency.MONTHLY, tzid: 'UTC', interval: 2 }, }, title: 'Scheduled report 3', - notification: {}, }, ]; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx index bb5c3a572e17e..e48e8198ec877 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_link.tsx @@ -10,13 +10,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; -import type { ApplicationStart } from '@kbn/core/public'; import { ILM_POLICY_NAME } from '@kbn/reporting-common'; import { LocatorPublic, SerializableRecord } from '../../shared_imports'; interface Props { - navigateToUrl: ApplicationStart['navigateToUrl']; locator: LocatorPublic; } @@ -26,7 +24,7 @@ const i18nTexts = { }), }; -export const IlmPolicyLink: FunctionComponent = ({ locator, navigateToUrl }) => { +export const IlmPolicyLink: FunctionComponent = ({ locator }) => { return ( = ({ locator, navigateToUrl page: 'policy_edit', policyName: ILM_POLICY_NAME, }); - navigateToUrl(url); + window.open(url, '_blank'); + window.focus(); }} > {i18nTexts.buttonLabel} diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index 8913bcd722995..b57772c517592 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -19,11 +19,17 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { Frequency } from '@kbn/rrule'; import moment from 'moment'; import { stringify } from 'query-string'; import { REPORTING_REDIRECT_APP, buildKibanaPath } from '@kbn/reporting-common'; import type { ScheduledReportApiJSON, BaseParamsV2 } from '@kbn/reporting-common/types'; +import { + RecurrenceFrequency, + RecurringSchedule, +} from '@kbn/response-ops-recurring-schedule-form/types'; +import { RecurrenceEnd } from '@kbn/response-ops-recurring-schedule-form/constants'; +import type { Rrule } from '@kbn/task-manager-plugin/server/task'; import { ListingPropsInternal } from '..'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; import { useGetScheduledList } from '../hooks/use_get_scheduled_list'; @@ -31,6 +37,8 @@ import { prettyPrintJobType } from '../../../common/job_utils'; import { ReportScheduleIndicator } from './report_schedule_indicator'; import { useBulkDisable } from '../hooks/use_bulk_disable'; import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; +import { ScheduledReportFlyout } from './scheduled_report_flyout'; +import { ScheduledReport } from '../../types'; interface QueryParams { index: number; @@ -39,7 +47,7 @@ interface QueryParams { export const ReportSchedulesTable = (props: ListingPropsInternal) => { const { http, toasts } = props; - + const [selectedReport, setSelectedReport] = useState(null); const [queryParams, setQueryParams] = useState({ index: 1, size: 10, @@ -77,7 +85,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { }), width: '20%', render: (_title: string, item: ScheduledReportApiJSON) => ( - {}}> + setSelectedReport(item)}> {_title} ), @@ -183,7 +191,9 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { 'data-test-subj': (item) => `reportViewConfig-${item.id}`, type: 'icon', icon: 'calendar', - onClick: () => {}, + onClick: (item) => { + setSelectedReport(item); + }, }, { name: i18n.translate('xpack.reporting.schedules.table.openDashboard.title', { @@ -244,6 +254,70 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { [setQueryParams] ); + const isCustom = (rRule: Rrule) => { + const freq = rRule.freq; + // interval is greater than 1 + if (rRule.interval && rRule.interval > 1) { + return true; + } + // frequency is daily and no weekdays are selected + if (freq && freq === Frequency.DAILY && !rRule.byweekday) { + return true; + } + // frequency is weekly and there are multiple weekdays selected + if (freq && freq === Frequency.WEEKLY && rRule.byweekday && rRule.byweekday.length > 1) { + return true; + } + // frequency is monthly and by month day is selected + if (freq && freq === Frequency.MONTHLY && rRule.bymonthday) { + return true; + } + return false; + }; + + const transformedReport = (report: ScheduledReportApiJSON): ScheduledReport => { + const { title, schedule, notification } = report; + const rRule = schedule.rrule; + + const isCustomFrequency = isCustom(rRule); + const frequency = rRule.freq as RecurrenceFrequency; + + const recurringSchedule: RecurringSchedule = { + frequency: isCustomFrequency ? 'CUSTOM' : frequency, + interval: rRule.interval, + ends: RecurrenceEnd.NEVER, + }; + + if (isCustomFrequency) { + recurringSchedule.customFrequency = frequency; + } + + if (frequency !== Frequency.MONTHLY && rRule.byweekday) { + recurringSchedule.byweekday = rRule.byweekday.reduce>((acc, day) => { + acc[day] = true; + return acc; + }, {}); + } + if (frequency === Frequency.MONTHLY) { + if (rRule.byweekday) { + recurringSchedule.bymonth = 'weekday'; + } else if (rRule.bymonthday) { + recurringSchedule.bymonth = 'day'; + } + } + + return { + title, + recurringSchedule, + reportTypeId: report.jobtype as ScheduledReport['reportTypeId'], + timezone: schedule.rrule.tzid, + startDate: '', + recurring: true, + sendByEmail: Boolean(notification?.email), + emailRecipients: [...(notification?.email?.to || [])], + }; + }; + return ( @@ -261,6 +335,21 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { onChange={tableOnChangeCallback} rowProps={() => ({ 'data-test-subj': 'scheduledReportRow' })} /> + {selectedReport && ( + { + setSelectedReport(null); + }} + scheduledReport={transformedReport(selectedReport)} + availableReportTypes={[ + { + id: selectedReport.jobtype, + label: prettyPrintJobType(selectedReport.jobtype), + }, + ]} + /> + )} ); }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx index b18ed60cc8013..754fda718b440 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -158,9 +158,7 @@ export const ReportingTabs: React.FunctionComponent< {ilmPolicyContextValue?.isLoading ? ( ) : ( - showIlmPolicyLink && ( - - ) + showIlmPolicyLink && )} )} From eeaed70fe155f12e8350b1ef63e7d073b18e156e Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 19 Jun 2025 11:26:42 +0100 Subject: [PATCH 10/15] trim report title to 1 line --- .../components/report_exports_table.tsx | 13 ++++--- .../components/report_schedules_table.tsx | 3 +- .../management/components/truncated_title.tsx | 35 +++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/truncated_title.tsx diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 85ffda4642cad..3dfdb14ece847 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -31,6 +31,7 @@ import { Poller } from '../../../common/poller'; import { ReportDeleteButton, ReportInfoFlyout, ReportStatusIndicator } from '.'; import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; +import { TruncatedTitle } from './truncated_title'; type TableColumn = EuiBasicTableColumn; @@ -266,10 +267,14 @@ export class ReportExportsTable extends Component { data-test-subj={`viewReportingLink-${job.id}`} onClick={() => this.setState({ selectedJob: job })} > - {objectTitle || - i18n.translate('xpack.reporting.exports.table.noTitleLabel', { - defaultMessage: 'Untitled', - })} + ); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index b57772c517592..be1b20af5b87b 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -39,6 +39,7 @@ import { useBulkDisable } from '../hooks/use_bulk_disable'; import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; import { ScheduledReportFlyout } from './scheduled_report_flyout'; import { ScheduledReport } from '../../types'; +import { TruncatedTitle } from './truncated_title'; interface QueryParams { index: number; @@ -86,7 +87,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { width: '20%', render: (_title: string, item: ScheduledReportApiJSON) => ( setSelectedReport(item)}> - {_title} + ), mobileOptions: { diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/truncated_title.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/truncated_title.tsx new file mode 100644 index 0000000000000..3db9d556fbd0d --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/truncated_title.tsx @@ -0,0 +1,35 @@ +/* + * 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 { css } from '@emotion/react'; + +const LINE_CLAMP = 1; + +const getTextCss = css` + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: ${LINE_CLAMP}; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; +`; + +interface Props { + text: string; +} + +const TruncatedTitleComponent: React.FC = ({ text }) => { + return ( + + {text} + + ); +}; +TruncatedTitleComponent.displayName = 'TruncatedTitle'; + +export const TruncatedTitle = React.memo(TruncatedTitleComponent); From 1eae25dc2df8a4c8a907899e1b7dc771ff2906a6 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 19 Jun 2025 18:34:33 +0100 Subject: [PATCH 11/15] navigate to schedules tab after report scheduled --- .../private/kbn-reporting/common/constants.ts | 2 + .../components/report_exports_table.tsx | 1 + .../components/report_schedules_table.tsx | 76 ++----------------- .../components/scheduled_report_flyout.tsx | 9 ++- .../scheduled_report_flyout_content.tsx | 4 +- .../reporting_and_security/management.ts | 31 +++++++- 6 files changed, 50 insertions(+), 73 deletions(-) diff --git a/src/platform/packages/private/kbn-reporting/common/constants.ts b/src/platform/packages/private/kbn-reporting/common/constants.ts index 9803499f777ed..093868282a724 100644 --- a/src/platform/packages/private/kbn-reporting/common/constants.ts +++ b/src/platform/packages/private/kbn-reporting/common/constants.ts @@ -74,6 +74,8 @@ export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATO // Management UI route export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; +export const REPORTING_MANAGEMENT_SCHEDULES = + '/app/management/insightsAndAlerting/reporting/schedules'; /* * ILM diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx index 3dfdb14ece847..66d6d8fc0246c 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_exports_table.tsx @@ -395,6 +395,7 @@ export class ReportExportsTable extends Component { defaultMessage: 'View additional information about this report.', } ), + 'data-test-subj': 'reportViewInfoLink', type: 'icon', icon: 'iInCircle', onClick: (job) => this.setState({ selectedJob: job }), diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index be1b20af5b87b..589fec8441e8e 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -31,7 +31,11 @@ import { import { RecurrenceEnd } from '@kbn/response-ops-recurring-schedule-form/constants'; import type { Rrule } from '@kbn/task-manager-plugin/server/task'; import { ListingPropsInternal } from '..'; -import { guessAppIconTypeFromObjectType, getDisplayNameFromObjectType } from '../utils'; +import { + guessAppIconTypeFromObjectType, + getDisplayNameFromObjectType, + transformScheduledReport, +} from '../utils'; import { useGetScheduledList } from '../hooks/use_get_scheduled_list'; import { prettyPrintJobType } from '../../../common/job_utils'; import { ReportScheduleIndicator } from './report_schedule_indicator'; @@ -84,7 +88,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { name: i18n.translate('xpack.reporting.schedules.tableColumns.reportTitle', { defaultMessage: 'Title', }), - width: '20%', + width: '17%', render: (_title: string, item: ScheduledReportApiJSON) => ( setSelectedReport(item)}> @@ -180,7 +184,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { name: i18n.translate('xpack.reporting.schedules.tableColumns.actionsTitle', { defaultMessage: 'Actions', }), - width: '5%', + width: '8%', actions: [ { name: i18n.translate('xpack.reporting.schedules.table.viewConfig.title', { @@ -255,70 +259,6 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { [setQueryParams] ); - const isCustom = (rRule: Rrule) => { - const freq = rRule.freq; - // interval is greater than 1 - if (rRule.interval && rRule.interval > 1) { - return true; - } - // frequency is daily and no weekdays are selected - if (freq && freq === Frequency.DAILY && !rRule.byweekday) { - return true; - } - // frequency is weekly and there are multiple weekdays selected - if (freq && freq === Frequency.WEEKLY && rRule.byweekday && rRule.byweekday.length > 1) { - return true; - } - // frequency is monthly and by month day is selected - if (freq && freq === Frequency.MONTHLY && rRule.bymonthday) { - return true; - } - return false; - }; - - const transformedReport = (report: ScheduledReportApiJSON): ScheduledReport => { - const { title, schedule, notification } = report; - const rRule = schedule.rrule; - - const isCustomFrequency = isCustom(rRule); - const frequency = rRule.freq as RecurrenceFrequency; - - const recurringSchedule: RecurringSchedule = { - frequency: isCustomFrequency ? 'CUSTOM' : frequency, - interval: rRule.interval, - ends: RecurrenceEnd.NEVER, - }; - - if (isCustomFrequency) { - recurringSchedule.customFrequency = frequency; - } - - if (frequency !== Frequency.MONTHLY && rRule.byweekday) { - recurringSchedule.byweekday = rRule.byweekday.reduce>((acc, day) => { - acc[day] = true; - return acc; - }, {}); - } - if (frequency === Frequency.MONTHLY) { - if (rRule.byweekday) { - recurringSchedule.bymonth = 'weekday'; - } else if (rRule.bymonthday) { - recurringSchedule.bymonth = 'day'; - } - } - - return { - title, - recurringSchedule, - reportTypeId: report.jobtype as ScheduledReport['reportTypeId'], - timezone: schedule.rrule.tzid, - startDate: '', - recurring: true, - sendByEmail: Boolean(notification?.email), - emailRecipients: [...(notification?.email?.to || [])], - }; - }; - return ( @@ -342,7 +282,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { onClose={() => { setSelectedReport(null); }} - scheduledReport={transformedReport(selectedReport)} + scheduledReport={transformScheduledReport(selectedReport)} availableReportTypes={[ { id: selectedReport.jobtype, diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout.tsx index 5cf81fcc80887..a211d67bc1c78 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout.tsx @@ -25,7 +25,14 @@ export const ScheduledReportFlyout = ({ onClose, }: ScheduledReportFlyoutProps) => { return ( - + ( - + {i18n.REPORTING_PAGE_LINK_TEXT} ), 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 dccda59fed44a..f3f362571146f 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/management.ts @@ -24,13 +24,13 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); - await PageObjects.common.navigateToApp('reporting'); + await PageObjects.common.navigateToApp('reporting', { path: '/exports' }); await testSubjects.missingOrFail('reportJobListing'); }); it('does allow user with reporting privileges', async () => { await reportingFunctional.loginReportingUser(); - await PageObjects.common.navigateToApp('reporting'); + await PageObjects.common.navigateToApp('reporting', { path: '/exports' }); await testSubjects.existOrFail('reportJobListing'); }); @@ -54,5 +54,32 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await PageObjects.dashboard.expectOnDashboard(dashboardTitle); }); + + it('Allows user to view report details', async () => { + await PageObjects.common.navigateToApp('reporting'); + await (await testSubjects.findAll('euiCollapsedItemActionsButton'))[0].click(); + + await (await testSubjects.find('reportViewInfoLink')).click(); + + await testSubjects.existOrFail('reportInfoFlyout'); + }); + + describe('Schedules', () => { + it('does allow user with reporting privileges o navigate to the Schedules tab', async () => { + await reportingFunctional.loginReportingUser(); + + await PageObjects.common.navigateToApp('reporting'); + await (await testSubjects.find('reportingTabs-schedules')).click(); + await testSubjects.existOrFail('reportSchedulesTable'); + }); + + it('does not allow user to access schedules that does not have reporting privileges', async () => { + await reportingFunctional.loginDataAnalyst(); + + await PageObjects.common.navigateToApp('reporting'); + await (await testSubjects.find('reportingTabs-schedules')).click(); + await testSubjects.missingOrFail('reportSchedulesTable'); + }); + }); }); }; From 1102d74a84edfe499473beb5268b7cfe53145fb1 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 20 Jun 2025 10:10:22 +0100 Subject: [PATCH 12/15] lint fix --- .../management/components/report_schedules_table.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index 589fec8441e8e..b7697341d93b7 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -19,17 +19,10 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Frequency } from '@kbn/rrule'; import moment from 'moment'; import { stringify } from 'query-string'; import { REPORTING_REDIRECT_APP, buildKibanaPath } from '@kbn/reporting-common'; import type { ScheduledReportApiJSON, BaseParamsV2 } from '@kbn/reporting-common/types'; -import { - RecurrenceFrequency, - RecurringSchedule, -} from '@kbn/response-ops-recurring-schedule-form/types'; -import { RecurrenceEnd } from '@kbn/response-ops-recurring-schedule-form/constants'; -import type { Rrule } from '@kbn/task-manager-plugin/server/task'; import { ListingPropsInternal } from '..'; import { guessAppIconTypeFromObjectType, @@ -42,7 +35,6 @@ import { ReportScheduleIndicator } from './report_schedule_indicator'; import { useBulkDisable } from '../hooks/use_bulk_disable'; import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; import { ScheduledReportFlyout } from './scheduled_report_flyout'; -import { ScheduledReport } from '../../types'; import { TruncatedTitle } from './truncated_title'; interface QueryParams { From b63d4440bf23bb0ae33c54ac7048a687ed87b1f6 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 20 Jun 2025 14:45:21 +0100 Subject: [PATCH 13/15] add disable confirmation modal --- .../public/lib/ilm_policy_status_context.tsx | 10 +++- .../disable_report_confirmation_modal.tsx | 52 +++++++++++++++++++ .../migrate_ilm_policy_callout/index.tsx | 2 +- .../report_schedules_table.test.tsx | 50 +++++++++++++++++- .../components/report_schedules_table.tsx | 37 ++++++++++++- .../management/components/reporting_tabs.tsx | 2 +- .../reporting_and_security/management.ts | 3 +- 7 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/disable_report_confirmation_modal.tsx diff --git a/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx b/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx index a6ac75b330152..0733fd276192b 100644 --- a/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx +++ b/x-pack/platform/plugins/private/reporting/public/lib/ilm_policy_status_context.tsx @@ -32,9 +32,17 @@ export const IlmPolicyStatusContextProvider: FC> = ({ export type UseIlmPolicyStatusReturn = ReturnType; -export const useIlmPolicyStatus = (): ContextValue => { +export const useIlmPolicyStatus = (isEnabled: boolean): ContextValue => { const ctx = useContext(IlmPolicyStatusContext); if (!ctx) { + if (!isEnabled) { + return { + status: undefined, + isLoading: false, + recheckStatus: () => {}, + }; + } + throw new Error('"useIlmPolicyStatus" can only be used inside of "IlmPolicyStatusContext"'); } return ctx; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/disable_report_confirmation_modal.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/disable_report_confirmation_modal.tsx new file mode 100644 index 0000000000000..e7ba0dc278d0a --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/disable_report_confirmation_modal.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 React from 'react'; +import { EuiConfirmModal, useGeneratedHtmlId } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface ConfirmDisableReportModalProps { + title: string; + message: string; + onCancel: () => void; + onConfirm: () => void; +} + +const DisableReportConfirmationModalComponent: React.FC = ({ + title, + message, + onCancel, + onConfirm, +}) => { + const titleId = useGeneratedHtmlId(); + + return ( + + {message} + + ); +}; +DisableReportConfirmationModalComponent.displayName = 'DisableReportConfirmationModal'; + +export const DisableReportConfirmationModal = React.memo(DisableReportConfirmationModalComponent); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx index 43c617b2ba972..d8b936b55d0df 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx @@ -20,7 +20,7 @@ interface Props { } export const MigrateIlmPolicyCallOut: FunctionComponent = ({ toasts }) => { - const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); + const { isLoading, recheckStatus, status } = useIlmPolicyStatus(true); if (isLoading || !status || status === 'ok') { return null; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx index 5cbbda7732e80..015bd3dc18d96 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.test.tsx @@ -85,6 +85,14 @@ describe('ReportSchedulesTable', () => { mutateAsync: bulkDisableScheduledReportsMock, }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -158,8 +166,42 @@ describe('ReportSchedulesTable', () => { expect(await screen.findAllByText('Disabled')).toHaveLength(1); }); + it('shows disable confirmation modal correctly', async () => { + (useGetScheduledList as jest.Mock).mockReturnValue({ + data: { + page: 3, + size: 10, + total: 3, + data: mockScheduledReports, + }, + isLoading: false, + }); + + render( + + + + + + ); + + expect(await screen.findAllByTestId('scheduledReportRow')).toHaveLength(3); + + userEvent.click((await screen.findAllByTestId('euiCollapsedItemActionsButton'))[0]); + + const firstReportDisable = await screen.findByTestId( + `reportDisableSchedule-${mockScheduledReports[0].id}` + ); + + expect(firstReportDisable).toBeInTheDocument(); + + userEvent.click(firstReportDisable, { pointerEventsCheck: 0 }); + + expect(await screen.findByTestId('confirm-disable-modal')).toBeInTheDocument(); + }); + it('disable schedule report correctly', async () => { - (useGetScheduledList as jest.Mock).mockReturnValueOnce({ + (useGetScheduledList as jest.Mock).mockReturnValue({ data: { page: 3, size: 10, @@ -187,7 +229,11 @@ describe('ReportSchedulesTable', () => { expect(firstReportDisable).toBeInTheDocument(); - userEvent.click(firstReportDisable); + userEvent.click(firstReportDisable, { pointerEventsCheck: 0 }); + + expect(await screen.findByTestId('confirm-disable-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByText('Disable')); await waitFor(() => { expect(bulkDisableScheduledReportsMock).toHaveBeenCalledWith({ diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index b7697341d93b7..2dcad7a84d9b3 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -36,6 +36,7 @@ import { useBulkDisable } from '../hooks/use_bulk_disable'; import { NO_CREATED_REPORTS_DESCRIPTION } from '../../translations'; import { ScheduledReportFlyout } from './scheduled_report_flyout'; import { TruncatedTitle } from './truncated_title'; +import { DisableReportConfirmationModal } from './disable_report_confirmation_modal'; interface QueryParams { index: number; @@ -45,6 +46,8 @@ interface QueryParams { export const ReportSchedulesTable = (props: ListingPropsInternal) => { const { http, toasts } = props; const [selectedReport, setSelectedReport] = useState(null); + const [configFlyOut, setConfigFlyOut] = useState(false); + const [disableFlyOut, setDisableFlyOut] = useState(false); const [queryParams, setQueryParams] = useState({ index: 1, size: 10, @@ -189,6 +192,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { type: 'icon', icon: 'calendar', onClick: (item) => { + setConfigFlyOut(true); setSelectedReport(item); }, }, @@ -233,13 +237,28 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { type: 'icon', icon: 'cross', onClick: (item) => { - bulkDisableScheduledReports({ ids: [item.id] }); + setSelectedReport(item); + setDisableFlyOut(true); }, }, ], }, ]; + const onConfirm = useCallback(() => { + if (selectedReport) { + bulkDisableScheduledReports({ ids: [selectedReport.id] }); + } + + setSelectedReport(null); + setDisableFlyOut(false); + }, [bulkDisableScheduledReports, setSelectedReport, selectedReport]); + + const onCancel = useCallback(() => { + setSelectedReport(null); + setDisableFlyOut(false); + }, [setSelectedReport]); + const tableOnChangeCallback = useCallback( ({ page }: { page: QueryParams }) => { setQueryParams((prev) => ({ @@ -268,11 +287,12 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { onChange={tableOnChangeCallback} rowProps={() => ({ 'data-test-subj': 'scheduledReportRow' })} /> - {selectedReport && ( + {selectedReport && configFlyOut && ( { setSelectedReport(null); + setConfigFlyOut(false); }} scheduledReport={transformScheduledReport(selectedReport)} availableReportTypes={[ @@ -283,6 +303,19 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { ]} /> )} + {selectedReport && disableFlyOut ? ( + + ) : null} ); }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx index 754fda718b440..33f00a6ebb6d1 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -59,7 +59,7 @@ export const ReportingTabs: React.FunctionComponent< } = useKibana(); const ilmLocator = shareService.url.locators.get('ILM_LOCATOR_ID'); - const ilmPolicyContextValue = useIlmPolicyStatus(); + const ilmPolicyContextValue = useIlmPolicyStatus(config.statefulSettings.enabled); const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); 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 f3f362571146f..263944393070a 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/management.ts @@ -77,8 +77,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await reportingFunctional.loginDataAnalyst(); await PageObjects.common.navigateToApp('reporting'); - await (await testSubjects.find('reportingTabs-schedules')).click(); - await testSubjects.missingOrFail('reportSchedulesTable'); + await testSubjects.missingOrFail('reportingTabs-schedules'); }); }); }); From 97ef64dde56c7cc78bf9c35ed61e9667827ffaf3 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 20 Jun 2025 18:31:46 +0100 Subject: [PATCH 14/15] Add license check and propmt --- .../management/components/license_prompt.tsx | 76 +++++++++++++++++++ .../components/report_schedules_table.tsx | 2 +- .../components/reporting_tabs.test.tsx | 7 +- .../management/components/reporting_tabs.tsx | 59 +++++++++----- 4 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/public/management/components/license_prompt.tsx diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/license_prompt.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/license_prompt.tsx new file mode 100644 index 0000000000000..a44bbc2c8b936 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/license_prompt.tsx @@ -0,0 +1,76 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPageTemplate, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/reporting-public'; + +const title = ( +

+ {i18n.translate('xpack.reporting.schedules.licenseCheck.title', { + defaultMessage: `Upgrade your license to use Machine Learning`, + })} +

+); + +export const LicensePrompt = React.memo(() => { + const { application } = useKibana().services; + + return ( + + + + + + {i18n.translate('xpack.reporting.schedules.licenseCheck.upgrade', { + defaultMessage: `Upgrade`, + })} + + + + + + + + {i18n.translate('xpack.reporting.schedules.licenseCheck.startTrial', { + defaultMessage: `Start a trial`, + })} + + + + + + } + /> + ); +}); +LicensePrompt.displayName = 'LicensePrompt'; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx index 2dcad7a84d9b3..2e48b9eeb0afa 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedules_table.tsx @@ -163,7 +163,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { responsive={false} > - + diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx index dcbb0013810f7..067397755677a 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.test.tsx @@ -63,9 +63,12 @@ describe('Reporting tabs', () => { message: '', }), }; + const mockUnsubscribe = jest.fn(); + // @ts-expect-error we don't need to provide all props for the test const license$ = { subscribe: (handler: unknown) => { - return (handler as Function)(validCheck); + (handler as Function)(validCheck); + return { unsubscribe: mockUnsubscribe }; }, } as Observable; @@ -145,6 +148,7 @@ describe('Reporting tabs', () => { afterEach(() => { jest.clearAllMocks(); + mockUnsubscribe.mockClear(); }); it('renders exports components', async () => { @@ -246,6 +250,7 @@ describe('Reporting tabs', () => { expect(await screen.findByTestId('screenshotDiagnosticLink')).toBeInTheDocument(); }); + it('does not show when image reporting not set in config', async () => { const mockNoImageConfig = { ...mockConfig, diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx index 33f00a6ebb6d1..e4b057851c86c 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/reporting_tabs.tsx @@ -11,16 +11,19 @@ import { i18n } from '@kbn/i18n'; import { Route, Routes } from '@kbn/shared-ux-router'; import { RouteComponentProps } from 'react-router-dom'; import { CoreStart, ScopedHistory } from '@kbn/core/public'; -import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { ILicense, LicenseType, LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { ClientConfigType, ReportingAPIClient, + checkLicense, useInternalApiClient, useKibana, } from '@kbn/reporting-public'; import { SharePluginStart } from '@kbn/share-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import useObservable from 'react-use/lib/useObservable'; +import { Observable } from 'rxjs'; import { suspendedComponentWithProps } from './suspended_component_with_props'; import { REPORTING_EXPORTS_PATH, REPORTING_SCHEDULES_PATH, Section } from '../../constants'; import ReportExportsTable from './report_exports_table'; @@ -29,6 +32,7 @@ import { ReportDiagnostic } from './report_diagnostic'; import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; import ReportSchedulesTable from './report_schedules_table'; +import { LicensePrompt } from './license_prompt'; export interface MatchParams { section: Section; @@ -62,6 +66,17 @@ export const ReportingTabs: React.FunctionComponent< const ilmPolicyContextValue = useIlmPolicyStatus(config.statefulSettings.enabled); const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); + const license = useObservable(license$ ?? new Observable(), null); + + const isAtLeast = useCallback( + (level: LicenseType) => { + if (!license) { + return { enableLinks: false, showLinks: false }; + } + return checkLicense(license.check('reporting', level)); + }, + [license] + ); const tabs = [ { @@ -78,6 +93,8 @@ export const ReportingTabs: React.FunctionComponent< }, ]; + const { enableLinks, showLinks } = isAtLeast('trial'); + const renderExportsList = useCallback(() => { return suspendedComponentWithProps( ReportExportsTable, @@ -107,22 +124,28 @@ export const ReportingTabs: React.FunctionComponent< const renderSchedulesList = useCallback(() => { return ( - - {suspendedComponentWithProps( - ReportSchedulesTable, - 'xl' - )({ - apiClient, - toasts: notifications.toasts, - license$, - config, - capabilities, - redirect: navigateToApp, - navigateToUrl, - urlService: shareService.url, - http, - })} - + <> + {enableLinks && showLinks ? ( + + {suspendedComponentWithProps( + ReportSchedulesTable, + 'xl' + )({ + apiClient, + toasts: notifications.toasts, + license$, + config, + capabilities, + redirect: navigateToApp, + navigateToUrl, + urlService: shareService.url, + http, + })} + + ) : ( + + )} + ); }, [ apiClient, @@ -134,6 +157,8 @@ export const ReportingTabs: React.FunctionComponent< navigateToUrl, shareService.url, http, + enableLinks, + showLinks, ]); const onSectionChange = (newSection: Section) => { From 4e644885a91b54dd79f1e7857eb44072dfbeea1e Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 20 Jun 2025 18:49:56 +0100 Subject: [PATCH 15/15] address feedback 1 --- .../components/report_schedule_indicator.tsx | 36 +++++++------------ .../components/report_schedules_table.tsx | 12 +++++-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx index e778468599b10..ab61c4d5cff58 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.tsx @@ -15,34 +15,24 @@ interface ReportScheduleIndicatorProps { schedule: ScheduledReportApiJSON['schedule']; } +const translations = { + [Frequency.DAILY]: i18n.translate('xpack.reporting.schedules.scheduleIndicator.daily', { + defaultMessage: 'Daily', + }), + [Frequency.WEEKLY]: i18n.translate('xpack.reporting.schedules.scheduleIndicator.weekly', { + defaultMessage: 'Weekly', + }), + [Frequency.MONTHLY]: i18n.translate('xpack.reporting.schedules.scheduleIndicator.monthly', { + defaultMessage: 'Monthly', + }), +}; + export const ReportScheduleIndicator: FC = ({ schedule }) => { if (!schedule || !schedule.rrule) { return null; } - let statusText: string; - - switch (schedule.rrule.freq) { - case Frequency.DAILY: - statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.daily', { - defaultMessage: 'Daily', - }); - break; - case Frequency.WEEKLY: - statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.weekly', { - defaultMessage: 'Weekly', - }); - break; - case Frequency.MONTHLY: - statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.monthly', { - defaultMessage: 'Monthly', - }); - break; - default: - statusText = i18n.translate('xpack.reporting.schedules.scheduleIndicator.unknown', { - defaultMessage: 'Unknown', - }); - } + const statusText = translations[schedule.rrule.freq]; return ( { name: i18n.translate('xpack.reporting.schedules.tableColumns.reportTitle', { defaultMessage: 'Title', }), - width: '17%', + width: '22%', render: (_title: string, item: ScheduledReportApiJSON) => ( - setSelectedReport(item)}> + { + setSelectedReport(item); + setConfigFlyOut(true); + }} + > ), @@ -122,7 +128,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { name: i18n.translate('xpack.reporting.schedules.tableColumns.scheduleTitle', { defaultMessage: 'Schedule', }), - width: '15%', + width: '10%', render: (_schedule: ScheduledReportApiJSON['schedule']) => ( ),