diff --git a/src/platform/packages/private/kbn-reporting/public/index.ts b/src/platform/packages/private/kbn-reporting/public/index.ts index aa7212aa831e6..437c268272ba7 100644 --- a/src/platform/packages/private/kbn-reporting/public/index.ts +++ b/src/platform/packages/private/kbn-reporting/public/index.ts @@ -20,6 +20,7 @@ export { checkLicense } from './license_check'; import type { CoreSetup, CoreStart, NotificationsStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { useKibana as _useKibana } from '@kbn/kibana-react-plugin/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; /* Services received through useKibana context @@ -35,6 +36,7 @@ export interface KibanaContext { share: SharePluginStart; actions: ActionsPublicPluginSetup; notifications: NotificationsStart; + license$: LicensingPluginStart['license$']; } export const useKibana = () => _useKibana(); 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 c655715a9b9d6..88b54c43b1159 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 @@ -120,11 +120,16 @@ describe('ReportingAPIClient', () => { describe('getScheduledReportInfo', () => { beforeEach(() => { - httpClient.get.mockResolvedValueOnce({ data: [{ id: '123', title: 'Scheduled Report 1' }] }); + httpClient.get.mockResolvedValueOnce({ + data: [ + { id: 'scheduled-report-1', title: 'Scheduled Report 1' }, + { id: 'scheduled-report-2', title: 'Schedule Report 2' }, + ], + }); }); it('should send a get request', async () => { - await apiClient.getScheduledReportInfo('123'); + await apiClient.getScheduledReportInfo('scheduled-report-1'); expect(httpClient.get).toHaveBeenCalledWith( expect.stringContaining('/internal/reporting/scheduled/list') @@ -132,8 +137,8 @@ describe('ReportingAPIClient', () => { }); it('should return a report', async () => { - await expect(apiClient.getScheduledReportInfo('123')).resolves.toEqual({ - id: '123', + await expect(apiClient.getScheduledReportInfo('scheduled-report-1')).resolves.toEqual({ + id: 'scheduled-report-1', title: 'Scheduled Report 1', }); }); 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 0733fd276192b..a6ac75b330152 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,17 +32,9 @@ export const IlmPolicyStatusContextProvider: FC> = ({ export type UseIlmPolicyStatusReturn = ReturnType; -export const useIlmPolicyStatus = (isEnabled: boolean): ContextValue => { +export const useIlmPolicyStatus = (): 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/__test__/report_listing.test.helpers.tsx b/x-pack/platform/plugins/private/reporting/public/management/__test__/report_listing.test.helpers.tsx index 883a1f19b637c..e670e56fbd333 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 @@ -126,13 +126,7 @@ export const createTestBed = registerTestBed( => { - const query: HttpFetchQuery = { page: index, size }; + const query: HttpFetchQuery = { page, size: perPage }; const res = await http.get<{ page: number; diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_wrapper.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_wrapper.tsx new file mode 100644 index 0000000000000..31d26504ff977 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/ilm_policy_wrapper.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { RouteComponentProps } from 'react-router-dom'; +import { ClientConfigType, ReportingAPIClient, useKibana } from '@kbn/reporting-public'; +import { Section } from '../../constants'; + +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'; + +export interface MatchParams { + section: Section; +} + +export interface ReportingTabsProps { + config: ClientConfigType; + apiClient: ReportingAPIClient; +} + +export const IlmPolicyWrapper: React.FunctionComponent< + Partial & ReportingTabsProps +> = (props) => { + const { config, apiClient } = props; + const { + services: { + application: { capabilities }, + share: { url: urlService }, + notifications, + }, + } = useKibana(); + + const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); + const ilmPolicyContextValue = useIlmPolicyStatus(); + const hasIlmPolicy = ilmPolicyContextValue?.status !== 'policy-not-found'; + const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); + + return ( + <> + + + {capabilities?.management?.data?.index_lifecycle_management && ( + + {ilmPolicyContextValue?.isLoading ? ( + + ) : ( + showIlmPolicyLink && + )} + + )} + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { IlmPolicyWrapper as default }; 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 d8b936b55d0df..43c617b2ba972 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(true); + const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); if (isLoading || !status || status === 'ok') { return null; 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 16e50a38faa2e..cef137b003f0b 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 @@ -262,7 +262,10 @@ export class ReportExportsTable extends Component { width: tableColumnWidths.title, render: (objectTitle: string, job) => { return ( -
+
css({ paddingTop: euiTheme.size.s })} + > this.setState({ selectedJob: job })} diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.test.tsx new file mode 100644 index 0000000000000..6756a0edf44ff --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/public/management/components/report_schedule_indicator.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Frequency } from '@kbn/rrule'; +import { ReportScheduleIndicator } from './report_schedule_indicator'; + +describe('ReportScheduleIndicator', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders daily schedule indicator correctly', async () => { + render( + + ); + + expect(await screen.findByTestId('reportScheduleIndicator-3')).toBeInTheDocument(); + expect(await screen.findByText('Daily')).toBeInTheDocument(); + }); + + it('renders weekly schedule indicator correctly', async () => { + render( + + ); + + expect(await screen.findByTestId('reportScheduleIndicator-2')).toBeInTheDocument(); + expect(await screen.findByText('Weekly')).toBeInTheDocument(); + }); + + it('renders monthly schedule indicator correctly', async () => { + render( + + ); + + expect(await screen.findByTestId('reportScheduleIndicator-1')).toBeInTheDocument(); + expect(await screen.findByText('Monthly')).toBeInTheDocument(); + }); + + it('returns null when no frequency do not match', async () => { + render( + + ); + + expect(screen.queryByTestId('reportScheduleIndicator-0')).not.toBeInTheDocument(); + expect(screen.queryByText('Yearly')).not.toBeInTheDocument(); + }); + + it('returns null when no rrule', async () => { + // @ts-expect-error we don't need to provide all props for the test + const res = render(); + + expect(res.container.getElementsByClassName('euiBadge').length).toBe(0); + }); +}); 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 ab61c4d5cff58..59c6058bff17f 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 @@ -34,6 +34,10 @@ export const ReportScheduleIndicator: FC = ({ sche const statusText = translations[schedule.rrule.freq]; + if (!statusText) { + return null; + } + return ( { - const { http, toasts } = props; +export const ReportSchedulesTable = (props: { apiClient: ReportingAPIClient }) => { + const { apiClient } = props; + const { http } = useKibana().services; + const [selectedReport, setSelectedReport] = useState(null); - const [configFlyOut, setConfigFlyOut] = useState(false); - const [disableFlyOut, setDisableFlyOut] = useState(false); + const [isConfigFlyOutOpen, setIsConfigFlyOutOpen] = useState(false); + const [isDisableModalConfirmationOpen, setIsDisableModalConfirmationOpen] = + useState(false); const [queryParams, setQueryParams] = useState({ - index: 1, - size: 10, + page: 1, + perPage: 50, }); const { data: scheduledList, isLoading } = useGetScheduledList({ - http, ...queryParams, }); - const { mutateAsync: bulkDisableScheduledReports } = useBulkDisable({ - http, - toasts, - }); + const { mutateAsync: bulkDisableScheduledReports } = useBulkDisable(); const sortedList = orderBy(scheduledList?.data || [], ['created_at'], ['desc']); @@ -72,12 +72,12 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Type', }), width: '5%', - render: (_objectType: string) => ( + render: (objectType: string) => ( ), }, @@ -87,15 +87,14 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Title', }), width: '22%', - render: (_title: string, item: ScheduledReportApiJSON) => ( + render: (title: string, item: ScheduledReportApiJSON) => ( { - setSelectedReport(item); - setConfigFlyOut(true); + setReportAndOpenConfigFlyout(item); }} > - + ), mobileOptions: { @@ -113,7 +112,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { return ( {item.enabled ? i18n.translate('xpack.reporting.schedules.status.active', { @@ -132,8 +131,8 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Schedule', }), width: '10%', - render: (_schedule: ScheduledReportApiJSON['schedule']) => ( - + render: (schedule: ScheduledReportApiJSON['schedule']) => ( + ), }, { @@ -142,8 +141,8 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Next schedule', }), width: '20%', - render: (_nextRun: string, item) => { - return item.enabled ? moment(_nextRun).format('YYYY-MM-DD @ hh:mm A') : '—'; + render: (nextRun: string, item) => { + return item.enabled ? moment(nextRun).format('YYYY-MM-DD @ hh:mm A') : '—'; }, }, { @@ -152,7 +151,7 @@ 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, }, @@ -163,7 +162,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { defaultMessage: 'Created by', }), width: '15%', - render: (_createdBy: string) => { + render: (createdBy: string) => { return ( { responsive={false} > - + - {_createdBy} + {createdBy} @@ -200,18 +199,22 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { 'data-test-subj': (item) => `reportViewConfig-${item.id}`, type: 'icon', icon: 'calendar', - onClick: (item) => { - setConfigFlyOut(true); - setSelectedReport(item); - }, + onClick: (item) => setReportAndOpenConfigFlyout(item), }, { - 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', - }), + name: (item) => + i18n.translate('xpack.reporting.schedules.table.openDashboard.title', { + defaultMessage: 'Open Dashboard', + }), + description: (item) => + i18n.translate('xpack.reporting.schedules.table.openDashboard.description', { + defaultMessage: 'Open associated {objectType}', + values: { + objectType: item.payload?.objectType + ? getDisplayNameFromObjectType(item.payload?.objectType) + : '', + }, + }), 'data-test-subj': (item) => `reportOpenDashboard-${item.id}`, type: 'icon', icon: 'dashboardApp', @@ -245,35 +248,57 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { enabled: (item) => item.enabled, type: 'icon', icon: 'cross', - onClick: (item) => { - setSelectedReport(item); - setDisableFlyOut(true); - }, + onClick: (item) => setReportAndOpenDisableModal(item), }, ], }, ]; + const setReportAndOpenConfigFlyout = useCallback( + (report: ScheduledReportApiJSON) => { + setSelectedReport(report); + setIsConfigFlyOutOpen(true); + }, + [setSelectedReport, setIsConfigFlyOutOpen] + ); + + const unSetReportAndCloseConfigFlyout = useCallback(() => { + setSelectedReport(null); + setIsConfigFlyOutOpen(false); + }, [setSelectedReport, setIsConfigFlyOutOpen]); + + const setReportAndOpenDisableModal = useCallback( + (report: ScheduledReportApiJSON) => { + setSelectedReport(report); + setIsDisableModalConfirmationOpen(true); + }, + [setSelectedReport, setIsDisableModalConfirmationOpen] + ); + + const unSetReportAndCloseDisableModal = useCallback(() => { + setSelectedReport(null); + setIsDisableModalConfirmationOpen(false); + }, [setSelectedReport, setIsDisableModalConfirmationOpen]); + const onConfirm = useCallback(() => { if (selectedReport) { bulkDisableScheduledReports({ ids: [selectedReport.id] }); } + unSetReportAndCloseDisableModal(); + }, [selectedReport, bulkDisableScheduledReports, unSetReportAndCloseDisableModal]); - setSelectedReport(null); - setDisableFlyOut(false); - }, [bulkDisableScheduledReports, setSelectedReport, selectedReport]); - - const onCancel = useCallback(() => { - setSelectedReport(null); - setDisableFlyOut(false); - }, [setSelectedReport]); + const onCancel = useCallback( + () => unSetReportAndCloseDisableModal(), + [unSetReportAndCloseDisableModal] + ); const tableOnChangeCallback = useCallback( - ({ page }: { page: QueryParams }) => { + (criteria: CriteriaWithPagination) => { + const { index: page, size: perPage } = criteria.page; setQueryParams((prev) => ({ ...prev, - index: page.index + 1, - size: page.size, + page: page + 1, + perPage, })); }, [setQueryParams] @@ -288,20 +313,19 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { columns={tableColumns} loading={isLoading} pagination={{ - pageIndex: queryParams.index - 1, - pageSize: queryParams.size, + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, totalItemCount: scheduledList?.total ?? 0, }} noItemsMessage={NO_CREATED_REPORTS_DESCRIPTION} onChange={tableOnChangeCallback} rowProps={() => ({ 'data-test-subj': 'scheduledReportRow' })} /> - {selectedReport && configFlyOut && ( + {selectedReport && isConfigFlyOutOpen && ( { - setSelectedReport(null); - setConfigFlyOut(false); + unSetReportAndCloseConfigFlyout(); }} scheduledReport={transformScheduledReport(selectedReport)} availableReportTypes={[ @@ -312,7 +336,7 @@ export const ReportSchedulesTable = (props: ListingPropsInternal) => { ]} /> )} - {selectedReport && disableFlyOut ? ( + {selectedReport && isDisableModalConfirmationOpen ? ( { return () =>
{'Render Report Exports Table'}
; @@ -127,7 +127,16 @@ describe('Reporting tabs', () => { application, uiSettings: uiSettingsClient, data: dataService, - share: shareService, + share: { + shareService, + url: { + ...sharePluginMock.createStartContract().url, + locators: { + get: () => ilmLocator, + }, + }, + }, + notifications: notificationServiceMock.createStartContract(), }} > @@ -152,7 +161,7 @@ describe('Reporting tabs', () => { }); it('renders exports components', async () => { - await act(async () => render(renderComponent(props))); + render(renderComponent(props)); expect(await screen.findByTestId('reportingTabs-exports')).toBeInTheDocument(); expect(await screen.findByTestId('reportingTabs-schedules')).toBeInTheDocument(); @@ -172,9 +181,7 @@ describe('Reporting tabs', () => { }, }; - await act(async () => { - render(renderComponent({ ...props, ...updatedProps })); - }); + render(renderComponent({ ...props, ...updatedProps })); expect(await screen.findAllByRole('tab')).toHaveLength(2); }); @@ -202,10 +209,8 @@ describe('Reporting tabs', () => { }, }; - await act(async () => { - // @ts-expect-error we don't need to provide all props for the test - render(renderComponent({ ...props, shareService: updatedShareService })); - }); + // @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(); }); @@ -233,10 +238,8 @@ describe('Reporting tabs', () => { }; 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 })); - }); + // @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(); }); @@ -244,9 +247,7 @@ describe('Reporting tabs', () => { describe('Screenshotting Diagnostic', () => { it('shows screenshotting diagnostic link if config is stateful', async () => { - await act(async () => { - render(renderComponent(props)); - }); + render(renderComponent(props)); expect(await screen.findByTestId('screenshotDiagnosticLink')).toBeInTheDocument(); }); @@ -261,14 +262,12 @@ describe('Reporting tabs', () => { }, }; - await act(async () => { - render( - renderComponent({ - ...props, - config: mockNoImageConfig, - }) - ); - }); + render( + renderComponent({ + ...props, + config: mockNoImageConfig, + }) + ); expect(screen.queryByTestId('screenshotDiagnosticLink')).not.toBeInTheDocument(); }); 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 67a26e178dc38..1d65b1b6e2c3e 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,77 +5,51 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageTemplate, -} from '@elastic/eui'; +import React, { Suspense, useMemo } from 'react'; +import { EuiBetaBadge, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; 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 { ILicense, LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { - ClientConfigType, - ReportingAPIClient, - useInternalApiClient, - useKibana, -} from '@kbn/reporting-public'; -import { SharePluginStart } from '@kbn/share-plugin/public'; +import { useHistory, useParams } from 'react-router-dom'; +import { ILicense } from '@kbn/licensing-plugin/public'; +import { ClientConfigType, useInternalApiClient, useKibana } from '@kbn/reporting-public'; import { FormattedMessage } from '@kbn/i18n-react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { SCHEDULED_REPORT_VALID_LICENSES } from '@kbn/reporting-common'; -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'; import { LicensePrompt } from './license_prompt'; import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../translations'; +import IlmPolicyWrapper from './ilm_policy_wrapper'; 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, ...rest } = props; - const { notifications } = coreStart; - const { section } = rest.match?.params as MatchParams; - const history = rest.history as ScopedHistory; +export const ReportingTabs: React.FunctionComponent<{ config: ClientConfigType }> = ({ + config, +}) => { + const { section } = useParams(); + const history = useHistory(); + const { apiClient } = useInternalApiClient(); const { services: { application: { capabilities, navigateToApp, navigateToUrl }, http, + notifications, + share: { url: urlService }, + license$, }, } = useKibana(); - - const ilmLocator = shareService.url.locators.get('ILM_LOCATOR_ID'); - 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 hasValidLicense = useCallback(() => { + const licensingInfo = useMemo(() => { if (!license) { return { enableLinks: false, showLinks: false }; } @@ -127,73 +101,7 @@ export const ReportingTabs: React.FunctionComponent< }, ]; - const { enableLinks, showLinks } = hasValidLicense(); - - const renderExportsList = useCallback(() => { - return suspendedComponentWithProps( - ReportExportsTable, - 'xl' - )({ - apiClient, - toasts: notifications.toasts, - license$, - config, - capabilities, - redirect: navigateToApp, - navigateToUrl, - urlService: shareService.url, - http, - }); - }, [ - apiClient, - notifications.toasts, - license$, - config, - capabilities, - navigateToApp, - navigateToUrl, - shareService.url, - http, - ]); - - const renderSchedulesList = useCallback(() => { - return ( - <> - {enableLinks && showLinks ? ( - - {suspendedComponentWithProps( - ReportSchedulesTable, - 'xl' - )({ - apiClient, - toasts: notifications.toasts, - license$, - config, - capabilities, - redirect: navigateToApp, - navigateToUrl, - urlService: shareService.url, - http, - })} - - ) : ( - - )} - - ); - }, [ - apiClient, - notifications.toasts, - license$, - config, - capabilities, - navigateToApp, - navigateToUrl, - shareService.url, - http, - enableLinks, - showLinks, - ]); + const { enableLinks, showLinks } = licensingInfo; const onSectionChange = (newSection: Section) => { history.push(`/${newSection}`); @@ -206,23 +114,7 @@ export const ReportingTabs: React.FunctionComponent< bottomBorder rightSideItems={ config.statefulSettings.enabled - ? [ - , - - - , - - {capabilities?.management?.data?.index_lifecycle_management && ( - - {ilmPolicyContextValue?.isLoading ? ( - - ) : ( - showIlmPolicyLink && - )} - - )} - , - ] + ? [] : [] } data-test-subj="reportingPageHeader" @@ -258,8 +150,38 @@ export const ReportingTabs: React.FunctionComponent< /> - - + ( + }> + + + )} + /> + ( + }> + {enableLinks && showLinks ? ( + + ) : ( + + )} + + )} + /> ); diff --git a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx index 729a121e4a9e0..e7ab3a324a285 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/components/scheduled_report_flyout_content.tsx @@ -405,6 +405,7 @@ export const ScheduledReportFlyoutContent = ({ ({ + useKibana: jest.fn(), +})); jest.mock('../apis/bulk_disable_scheduled_reports', () => ({ bulkDisableScheduledReports: jest.fn(), @@ -26,6 +31,14 @@ describe('useBulkDisable', () => { ); beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + http, + notifications: { + toasts, + }, + }, + }); jest.clearAllMocks(); }); @@ -36,7 +49,7 @@ describe('useBulkDisable', () => { total: 1, }); - const { result } = renderHook(() => useBulkDisable({ http, toasts }), { + const { result } = renderHook(() => useBulkDisable(), { wrapper, }); @@ -59,7 +72,7 @@ describe('useBulkDisable', () => { it('throws error', async () => { (bulkDisableScheduledReports as jest.Mock).mockRejectedValueOnce({}); - const { result } = renderHook(() => useBulkDisable({ http, toasts }), { + const { result } = renderHook(() => useBulkDisable(), { wrapper, }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx index b0ab70d05093d..b0927805efd89 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_bulk_disable.tsx @@ -6,18 +6,22 @@ */ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { HttpSetup, IHttpFetchError, ResponseErrorBody, ToastsStart } from '@kbn/core/public'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/reporting-public'; import { bulkDisableScheduledReports } from '../apis/bulk_disable_scheduled_reports'; -import { mutationKeys, queryKeys } from '../query_keys'; +import { mutationKeys } from '../mutation_keys'; export type ServerError = IHttpFetchError; const getKey = mutationKeys.bulkDisableScheduledReports; -export const useBulkDisable = (props: { http: HttpSetup; toasts: ToastsStart }) => { - const { http, toasts } = props; +export const useBulkDisable = () => { const queryClient = useQueryClient(); + const { + http, + notifications: { toasts }, + } = useKibana().services; return useMutation({ mutationKey: getKey(), @@ -40,7 +44,7 @@ export const useBulkDisable = (props: { http: HttpSetup; toasts: ToastsStart }) }) ); queryClient.invalidateQueries({ - queryKey: queryKeys.getScheduledList({}), + queryKey: ['reporting', 'scheduledList'], 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 index d7ea38502e41c..f41e13f4b58dc 100644 --- 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 @@ -12,6 +12,11 @@ import { renderHook, waitFor } from '@testing-library/react'; import { getScheduledReportsList } from '../apis/get_scheduled_reports_list'; import { useGetScheduledList } from './use_get_scheduled_list'; import { testQueryClient } from '../test_utils/test_query_client'; +import { useKibana } from '@kbn/reporting-public'; + +jest.mock('@kbn/reporting-public', () => ({ + useKibana: jest.fn(), +})); jest.mock('../apis/get_scheduled_reports_list', () => ({ getScheduledReportsList: jest.fn(), @@ -26,12 +31,17 @@ describe('useGetScheduledList', () => { beforeEach(() => { jest.clearAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + http, + }, + }); }); it('calls getScheduledList with correct arguments', async () => { (getScheduledReportsList as jest.Mock).mockResolvedValueOnce({ data: [] }); - const { result } = renderHook(() => useGetScheduledList({ http, index: 1, size: 10 }), { + const { result } = renderHook(() => useGetScheduledList({}), { wrapper, }); @@ -41,8 +51,8 @@ describe('useGetScheduledList', () => { expect(getScheduledReportsList).toBeCalledWith({ http, - index: 1, - size: 10, + page: 1, + perPage: 50, }); }); }); diff --git a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx index 34cde02bcb0c8..604a6553e4c0f 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/hooks/use_get_scheduled_list.tsx @@ -6,23 +6,24 @@ */ import { useQuery } from '@tanstack/react-query'; -import { HttpSetup } from '@kbn/core/public'; +import { useKibana } from '@kbn/reporting-public'; import { getScheduledReportsList } from '../apis/get_scheduled_reports_list'; import { queryKeys } from '../query_keys'; export const getKey = queryKeys.getScheduledList; interface GetScheduledListQueryProps { - http: HttpSetup; - index?: number; - size?: number; + page?: number; + perPage?: number; } export const useGetScheduledList = (props: GetScheduledListQueryProps) => { - const { index = 1, size = 10 } = props; + const { http } = useKibana().services; + + const { page = 1, perPage = 50 } = props; return useQuery({ - queryKey: getKey({ index, size }), - queryFn: () => getScheduledReportsList(props), + queryKey: getKey({ page, perPage }), + queryFn: () => getScheduledReportsList({ http, page, perPage }), keepPreviousData: true, }); }; diff --git a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx index cf73cf7890f3d..05aa2c94815eb 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx @@ -45,7 +45,7 @@ export const createScheduledReportShareIntegration = ({ const { sharingData } = shareOpts as unknown as { sharingData: ReportingSharingData }; return { label: ({ openFlyout }) => ( - + {SCHEDULE_EXPORT_BUTTON_LABEL} ), 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 7d1917fa4bdbd..07c4361fee5ef 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 @@ -61,6 +61,7 @@ export async function mountManagementSection({ docLinks: coreStart.docLinks, data: dataService, share: shareService, + license$, actions: actionsService, notifications: notificationsService, }; @@ -82,15 +83,7 @@ export async function mountManagementSection({ render={(routerProps) => { return ( }> - + ); }} diff --git a/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts b/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts index b1d2acc130f74..a9782f5f088d8 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts +++ b/x-pack/platform/plugins/private/reporting/public/management/mutation_keys.ts @@ -8,4 +8,5 @@ export const mutationKeys = { root: 'reporting', scheduleReport: () => [mutationKeys.root, 'scheduleReport'] as const, + bulkDisableScheduledReports: () => [mutationKeys.root, 'bulkDisableScheduledReports'] as const, }; 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 8ada852e0738e..a854608584fdb 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 @@ -11,7 +11,3 @@ export const queryKeys = { getHealth: () => [root, 'health'] as const, getUserProfile: () => [root, 'userProfile'] as const, }; - -export const mutationKeys = { - bulkDisableScheduledReports: () => [root, 'bulkDisableScheduledReports'] as const, -}; diff --git a/x-pack/platform/test/reporting_functional/reporting_and_security/management.ts b/x-pack/platform/test/reporting_functional/reporting_and_security/management.ts index 5302e9c752c1d..92c60d67f712a 100644 --- a/x-pack/platform/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/platform/test/reporting_functional/reporting_and_security/management.ts @@ -5,79 +5,169 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService, getPageObjects }: FtrProviderContext) => { - const PageObjects = getPageObjects(['common', 'reporting', 'dashboard']); + const PageObjects = getPageObjects(['common', 'reporting', 'dashboard', 'security', 'exports']); + const toasts = getService('toasts'); + const find = getService('find'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); const reportingFunctional = getService('reportingFunctional'); + const reportingApi = getService('reportingAPI'); + const retry = getService('retry'); - // Failing: See https://github.com/elastic/kibana/issues/225172 - describe.skip('Access to Management > Reporting', () => { + describe('Access to Management > Reporting', () => { before(async () => { await reportingFunctional.initEcommerce(); }); after(async () => { await reportingFunctional.teardownEcommerce(); + await reportingApi.deleteAllReports(); }); - it('does not allow user that does not have reporting privileges', async () => { - await reportingFunctional.loginDataAnalyst(); - await PageObjects.common.navigateToApp('reporting', { path: '/exports' }); - await testSubjects.missingOrFail('reportJobListing'); - }); + describe('Exports', () => { + it('does allow user with reporting privileges', async () => { + await reportingFunctional.loginReportingUser(); + await PageObjects.common.navigateToApp('reporting'); + await testSubjects.existOrFail('reportJobListing'); + }); - it('does allow user with reporting privileges', async () => { - await reportingFunctional.loginReportingUser(); - await PageObjects.common.navigateToApp('reporting', { path: '/exports' }); - await testSubjects.existOrFail('reportJobListing'); - }); + it('Allows users to navigate back to where a report was generated', async () => { + const dashboardTitle = 'Ecom Dashboard'; + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard(dashboardTitle); - it('Allows users to navigate back to where a report was generated', async () => { - const dashboardTitle = 'Ecom Dashboard'; - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard(dashboardTitle); + await PageObjects.reporting.selectExportItem('PDF'); + await PageObjects.reporting.clickGenerateReportButton(); - await PageObjects.reporting.selectExportItem('PDF'); - await PageObjects.reporting.clickGenerateReportButton(); + await PageObjects.common.navigateToApp('reporting'); + await PageObjects.common.sleep(3000); // Wait an amount of time for auto-polling to refresh the jobs - await PageObjects.common.navigateToApp('reporting'); - await PageObjects.common.sleep(3000); // Wait an amount of time for auto-polling to refresh the jobs + // We do not need to wait for the report to finish generating + await (await testSubjects.find('euiCollapsedItemActionsButton')).click(); + await (await testSubjects.find('reportOpenInKibanaApp')).click(); - // We do not need to wait for the report to finish generating - await (await testSubjects.find('euiCollapsedItemActionsButton')).click(); - await (await testSubjects.find('reportOpenInKibanaApp')).click(); + const [, dashboardWindowHandle] = await browser.getAllWindowHandles(); + await browser.switchToWindow(dashboardWindowHandle); - const [, dashboardWindowHandle] = await browser.getAllWindowHandles(); - await browser.switchToWindow(dashboardWindowHandle); + await PageObjects.dashboard.expectOnDashboard(dashboardTitle); + }); - await PageObjects.dashboard.expectOnDashboard(dashboardTitle); - }); + it('Allows user to view report details', async () => { + await retry.try(async () => { + await PageObjects.common.navigateToApp('reporting'); + }); - it('Allows user to view report details', async () => { - await PageObjects.common.navigateToApp('reporting'); - await (await testSubjects.findAll('euiCollapsedItemActionsButton'))[0].click(); + await testSubjects.existOrFail('reportJobListing'); - await (await testSubjects.find('reportViewInfoLink')).click(); + await (await testSubjects.findAll('euiCollapsedItemActionsButton'))[0].click(); - await testSubjects.existOrFail('reportInfoFlyout'); + await (await testSubjects.find('reportViewInfoLink')).click(); + + await testSubjects.existOrFail('reportInfoFlyout'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('reportInfoFlyout'); + }); }); describe('Schedules', () => { + const dashboardTitle = 'Ecom Dashboard'; it('does allow user with reporting privileges to navigate to the Schedules tab', async () => { - await reportingFunctional.loginReportingUser(); + await retry.try(async () => { + await PageObjects.common.navigateToApp('reporting'); + }); - 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(); + it('allows user to navigate to schedules tab from where report was generated', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard(dashboardTitle); + + await PageObjects.exports.clickExportTopNavButton(); + await (await testSubjects.find('scheduleExport')).click(); + await testSubjects.existOrFail('exportItemDetailsFlyout'); + + await (await testSubjects.find('scheduleExportSubmitButton')).click(); + + const successToast = await toasts.getElementByIndex(1); + expect(await successToast.getVisibleText()).to.contain( + 'Find your schedule information and your exports in the Reporting page' + ); + await toasts.dismissAll(); await PageObjects.common.navigateToApp('reporting'); + await (await testSubjects.find('reportingTabs-schedules')).click(); + await testSubjects.existOrFail('reportSchedulesTable'); + + const title = await testSubjects.getVisibleText('reportTitle'); + expect(title).to.contain(dashboardTitle); + }); + + it('allows user to view schedule config in flyout', async () => { + await testSubjects.existOrFail('reportSchedulesTable'); + await (await testSubjects.findAll('euiCollapsedItemActionsButton'))[0].click(); + const viewConfigButton = await find.byCssSelector(`[data-test-subj*="reportViewConfig-"]`); + + await viewConfigButton.click(); + + await testSubjects.existOrFail('scheduledReportFlyout'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('scheduledReportFlyout'); + }); + + it('allows user to disable schedule', async () => { + await testSubjects.existOrFail('reportSchedulesTable'); + await (await testSubjects.findAll('euiCollapsedItemActionsButton'))[0].click(); + const disableButton = await find.byCssSelector( + `[data-test-subj*="reportDisableSchedule-"]` + ); + + await disableButton.click(); + + await testSubjects.existOrFail('confirm-disable-modal'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirm-disable-modal'); + + const successToast = await toasts.getElementByIndex(1); + expect(await successToast.getVisibleText()).to.contain('Scheduled report disabled'); + await toasts.dismissAll(); + + await testSubjects.existOrFail('reportStatus-disabled'); + }); + + it('allows user to open dashboard', async () => { + await testSubjects.existOrFail('reportSchedulesTable'); + await (await testSubjects.findAll('euiCollapsedItemActionsButton'))[0].click(); + const openDashboardButton = await find.byCssSelector( + `[data-test-subj*="reportOpenDashboard-"]` + ); + + await openDashboardButton.click(); + + const [, , dashboardWindowHandle] = await browser.getAllWindowHandles(); // it is the third window handle + + await browser.switchToWindow(dashboardWindowHandle); + + await PageObjects.dashboard.expectOnDashboard(dashboardTitle); + }); + }); + + describe('non privilege user', () => { + it('does not allow user that does not have reporting privileges', async () => { + await retry.try(async () => { + await reportingFunctional.loginDataAnalyst(); + await PageObjects.common.navigateToApp('reporting'); + }); + + await testSubjects.missingOrFail('reportJobListing'); + }); + + it('does not allow user to access schedules that does not have reporting privileges', async () => { await testSubjects.missingOrFail('reportingTabs-schedules'); }); }); diff --git a/x-pack/platform/test/reporting_functional/reporting_without_security/management.ts b/x-pack/platform/test/reporting_functional/reporting_without_security/management.ts index a46128b0398d5..2e09971e7dd5a 100644 --- a/x-pack/platform/test/reporting_functional/reporting_without_security/management.ts +++ b/x-pack/platform/test/reporting_functional/reporting_without_security/management.ts @@ -80,5 +80,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const tableCellText = await firstTitleElem.getVisibleText(); expect(tableCellText).to.be(`Tiểu thuyết`); }); + + describe('Schedules', () => { + it('allows user with reporting privileges to navigate to the Schedules tab', async () => { + await PageObjects.common.navigateToApp('reporting'); + await (await testSubjects.find('reportingTabs-schedules')).click(); + await testSubjects.existOrFail('reportSchedulesTable'); + }); + }); }); };