Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0fba547
Updating routes
adcoelho Oct 14, 2025
7a25938
Merge branch 'main' into scheduled-reports-refactor-3
adcoelho Oct 20, 2025
1db08bf
Merge remote-tracking branch 'upstream/main' into scheduled-reports-r…
adcoelho Oct 21, 2025
b77846d
bulk delete
adcoelho Oct 21, 2025
ab19d9a
WIP
adcoelho Oct 16, 2025
de07a2e
Initial commit.
adcoelho Oct 21, 2025
9413ed9
Merge remote-tracking branch 'upstream/main' into be-update-scheduled…
adcoelho Oct 21, 2025
8bad89d
Functional tests.
adcoelho Oct 21, 2025
ef5c0e9
Unit tests.
adcoelho Oct 21, 2025
059acde
fixing linting
adcoelho Oct 22, 2025
9773b70
use schema v3
adcoelho Oct 22, 2025
f5facf1
little refactor
adcoelho Oct 22, 2025
3cf6bcd
fixing types
adcoelho Oct 22, 2025
84ee662
Merge remote-tracking branch 'upstream/main' into fe-update-scheduled…
adcoelho Oct 30, 2025
d9a5d2a
wip
adcoelho Oct 30, 2025
b4e87f8
Merge remote-tracking branch 'upstream/main' into fe-update-scheduled…
adcoelho Nov 5, 2025
324f8e5
removing files
adcoelho Nov 5, 2025
a2c05a3
update edit form, add tests
js-jankisalvi Nov 13, 2025
c6860da
fix validation
js-jankisalvi Nov 14, 2025
22ad424
Merge branch 'main' into fe-update-scheduled-reports
js-jankisalvi Nov 14, 2025
e1bbc56
add hook tests, update tests
js-jankisalvi Nov 14, 2025
be79e9b
Merge branch 'main' into fe-update-scheduled-reports
js-jankisalvi Nov 14, 2025
a20d722
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 14, 2025
ece942f
Merge branch 'main' into fe-update-scheduled-reports
js-jankisalvi Nov 18, 2025
6138a5c
address feedback
js-jankisalvi Nov 18, 2025
2dd05ef
update query keys and tests
js-jankisalvi Nov 19, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface KibanaContext {
actions: ActionsPublicPluginSetup;
notifications: NotificationsStart;
license$: LicensingPluginStart['license$'];
userProfile: CoreStart['userProfile'];
}

export const useKibana = () => _useKibana<KibanaContext>();
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 type { HttpSetup } from '@kbn/core-http-browser';
import { INTERNAL_ROUTES } from '@kbn/reporting-common';
import type { RruleSchedule } from '@kbn/task-manager-plugin/server';
import type { ScheduledReportingJobResponse } from '../../../server/types';

export interface UpdateScheduleReportRequestParams {
reportId: string;
title?: string;
schedule?: RruleSchedule;
}

export const updateScheduleReport = ({
http,
params: { reportId, title, schedule },
}: {
http: HttpSetup;
params: UpdateScheduleReportRequestParams;
}) => {
return http.put<ScheduledReportingJobResponse>(`${INTERNAL_ROUTES.SCHEDULE_PREFIX}/${reportId}`, {
body: JSON.stringify({ title, schedule }),
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* 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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render, screen, waitFor } from '@testing-library/react';
import {
applicationServiceMock,
coreMock,
httpServiceMock,
notificationServiceMock,
} from '@kbn/core/public/mocks';
import { ReportingAPIClient, useKibana } from '@kbn/reporting-public';
import { mockScheduledReports } from '../../../common/test/fixtures';

import { CreateScheduledReportForm } from './create_scheduled_report_form';
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
import userEvent from '@testing-library/user-event';
import { getReportingHealth } from '../apis/get_reporting_health';
import { useGetUserProfileQuery } from '../hooks/use_get_user_profile_query';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import { scheduleReport } from '../apis/schedule_report';
import moment from 'moment';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';

jest.mock('@kbn/kibana-react-plugin/public');
jest.mock('@kbn/reporting-public', () => ({
useKibana: jest.fn(),
ReportingAPIClient: jest.fn().mockImplementation(() => ({
getDecoratedJobParams: jest.fn().mockResolvedValue({
browserTimezone: 'UTC',
version: 'x.x.x',
title: 'Scheduled report 2',
objectType: 'dashboard',
}),
})),
}));

jest.mock('../hooks/use_get_user_profile_query');
jest.mock('../apis/get_reporting_health');
jest.mock('../apis/schedule_report');

const mockValidateEmailAddresses = jest.fn().mockResolvedValue([]);
const mockReportingHealth = jest.mocked(getReportingHealth);
const mockGetUserProfileQuery = jest.mocked(useGetUserProfileQuery);
const mockedUseUiSetting = jest.mocked(useUiSetting);
const mockScheduleReport = jest.mocked(scheduleReport);

describe('createScheduledReportForm', () => {
const onClose = jest.fn();
const application = applicationServiceMock.createStartContract();
const http = httpServiceMock.createSetupContract();
const uiSettings = coreMock.createSetup().uiSettings;
const apiClient = new ReportingAPIClient(http, uiSettings, 'x.x.x');
const queryClient = new QueryClient();
let user: ReturnType<typeof userEvent.setup>;
const today = new Date('2025-11-10T12:00:00.000Z');

const defaultSharingData = {
title: 'canvas',
locatorParams: {
id: 'canvas-123',
params: {
id: '123',
},
},
};

const defaultProps = {
apiClient,
scheduledReport: mockScheduledReports[1],
availableReportTypes: [],
sharingData: defaultSharingData,
onClose,
};

beforeAll(() => {
moment.tz.setDefault('UTC');
mockedUseUiSetting.mockReturnValue('UTC');
});

beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
services: {
application: {
capabilities: { ...application.capabilities, manageReporting: { show: true } },
},
http,
notifications: notificationServiceMock.createStartContract(),
userProfile: userProfileServiceMock.createStart(),
actions: {
validateEmailAddresses: mockValidateEmailAddresses,
},
},
});
mockReportingHealth.mockResolvedValue({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
areNotificationsEnabled: true,
});
mockGetUserProfileQuery.mockReturnValue({
data: {
user: {
email: 'test@example.com',
username: 'testuser',
full_name: 'Test User',
},
uid: '123',
},
isLoading: false,
} as any);
mockScheduleReport.mockResolvedValue({
job: {
id: mockScheduledReports[1].id,
},
} as any);

jest.spyOn(Date, 'now').mockReturnValue(today.getTime());
});

afterEach(() => {
queryClient.clear();
jest.clearAllMocks();
});

afterAll(() => {
moment.tz.setDefault('Browser');
});

it('renders correctly', async () => {
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<CreateScheduledReportForm {...defaultProps} />
</QueryClientProvider>
</IntlProvider>
);

expect(await screen.findByTestId('scheduleExportForm')).toBeInTheDocument();
expect(await screen.findByTestId('scheduleExportSubmitButton')).toBeInTheDocument();
});

it('submits the form correctly', async () => {
user = userEvent.setup({ delay: null });
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<CreateScheduledReportForm
{...defaultProps}
availableReportTypes={[{ label: 'PDF', id: 'printablePdfV2' }]}
/>
</QueryClientProvider>
</IntlProvider>
);

const submitButton = await screen.findByTestId('scheduleExportSubmitButton');

await user.click(submitButton);
await waitFor(() => {
expect(mockScheduleReport).toHaveBeenCalledWith(
expect.objectContaining({
http,
params: {
reportTypeId: 'printablePdfV2',
schedule: {
rrule: {
byhour: [12],
byminute: [0],
byweekday: ['MO'],
freq: 3,
interval: 1,
dtstart: '2025-11-10T12:00:00.000Z',
tzid: 'UTC',
},
},
jobParams: expect.any(String),
notification: undefined,
},
})
);
});
});

it('cancels the form correctly', async () => {
user = userEvent.setup({ delay: null });
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<CreateScheduledReportForm {...defaultProps} />
</QueryClientProvider>
</IntlProvider>
);

const cancelButton = await screen.findByTestId('scheduleExportCancelButton');

await user.click(cancelButton);
await waitFor(() => {
expect(onClose).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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, { useMemo } from 'react';
import { convertToRRule } from '@kbn/response-ops-recurring-schedule-form/utils/convert_to_rrule';
import type { ReportingAPIClient } from '@kbn/reporting-public';
import { useKibana } from '@kbn/reporting-public';
import type { Rrule } from '@kbn/task-manager-plugin/server/task';
import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
import type { ReportingSharingData } from '@kbn/reporting-public/share/share_context_menu';
import { EuiLink } from '@elastic/eui';
import { REPORTING_MANAGEMENT_SCHEDULES } from '@kbn/reporting-common';
import type { ReportTypeData, ScheduledReport } from '../../types';
import type { FormData } from './scheduled_report_form';
import { ScheduledReportForm } from './scheduled_report_form';
import * as i18n from '../translations';
import { getReportParams } from '../report_params';
import { useScheduleReport } from '../hooks/use_schedule_report';
import { useGetUserProfileQuery } from '../hooks/use_get_user_profile_query';

export interface CreateScheduledReportFormProps {
// create
apiClient: ReportingAPIClient;
objectType?: string;
sharingData?: ReportingSharingData;
scheduledReport: Partial<ScheduledReport>;
availableReportTypes: ReportTypeData[];
onClose: () => void;
}

export const CreateScheduledReportForm = ({
apiClient,
objectType,
sharingData,
scheduledReport,
availableReportTypes,
onClose,
}: CreateScheduledReportFormProps) => {
const {
application: { capabilities },
http,
notifications: { toasts },
userProfile: userProfileService,
} = useKibana().services;
const { data: userProfile } = useGetUserProfileQuery({
userProfileService,
});
const reportingPageLink = useMemo(
() => (
<EuiLink href={http.basePath.prepend(REPORTING_MANAGEMENT_SCHEDULES)}>
{i18n.REPORTING_PAGE_LINK_TEXT}
</EuiLink>
),
[http.basePath]
);

const { mutateAsync: createScheduledReport, isLoading: isSubmitLoading } = useScheduleReport({
http,
});

const hasManageReportingPrivilege = useMemo(() => {
if (!capabilities) {
return false;
}
return capabilities.manageReporting.show === true;
}, [capabilities]);

const onSubmit = async (formData: FormData) => {
try {
const {
title,
reportTypeId,
startDate,
timezone,
recurringSchedule,
optimizedForPrinting,
sendByEmail,
emailRecipients,
} = formData;
const rrule = convertToRRule({
startDate,
timezone,
recurringSchedule,
includeTime: true,
});
await createScheduledReport({
reportTypeId,
jobParams: getReportParams({
apiClient,
// The assertion at the top of the component ensures these are defined when scheduling
sharingData: sharingData!,
objectType: objectType!,
title,
reportTypeId,
...(reportTypeId === 'printablePdfV2' ? { optimizedForPrinting } : {}),
}),
schedule: { rrule: rrule as Rrule },
notification: sendByEmail ? { email: { to: emailRecipients } } : undefined,
});
toasts.addSuccess({
title: i18n.SCHEDULED_REPORT_FORM_SUCCESS_TOAST_TITLE,
text: mountReactNode(
<>
{i18n.SCHEDULED_REPORT_FORM_SUCCESS_TOAST_MESSAGE} {reportingPageLink}.
</>
),
});
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
toasts.addError(error, {
title: i18n.SCHEDULED_REPORT_FORM_FAILURE_TOAST_TITLE,
toastMessage: i18n.SCHEDULED_REPORT_FORM_FAILURE_TOAST_MESSAGE,
});
// Forward error to signal whether to close the flyout or not
throw error;
}
};
return (
<ScheduledReportForm
scheduledReport={scheduledReport}
availableReportTypes={availableReportTypes}
onClose={onClose}
onSubmitForm={onSubmit}
isSubmitLoading={isSubmitLoading}
defaultEmail={
!hasManageReportingPrivilege && userProfile?.user.email ? userProfile.user.email : undefined
}
/>
);
};
Loading