Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

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 @@ -41,13 +41,12 @@ import { getPresets } from '../utils/get_presets';
import { getWeekdayInfo } from '../utils/get_weekday_info';
import { RecurringSchedule } from '../types';
import * as i18n from '../translations';
import { convertStringToMomentOptional, convertMomentToStringOptional } from '../converters/moment';

/**
* Using EuiForm in `div` mode since this is meant to be integrated in a larger form
*/
const UseField = getUseField({ component: Field });
export const toMoment = (value?: string): Moment | undefined => (value ? moment(value) : undefined);
export const toString = (value?: Moment): string => value?.toISOString() ?? '';

export interface RecurringScheduleFieldsProps {
startDate?: string;
Expand Down Expand Up @@ -211,8 +210,8 @@ export const RecurringScheduleFormFields = memo(
},
},
],
serializer: toString,
deserializer: toMoment,
serializer: convertMomentToStringOptional,
deserializer: convertStringToMomentOptional,
}}
componentProps={{
'data-test-subj': 'until-field',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import {
convertStringToMoment,
convertStringToMomentOptional,
convertMomentToString,
convertMomentToStringOptional,
} from './moment';
import moment from 'moment';

describe('Moment converters', () => {
describe('convertStringToMoment', () => {
it('should convert ISO string to Moment', () => {
const dateStr = '2025-06-26T12:34:56.789Z';
const result = convertStringToMoment(dateStr);
expect(moment.isMoment(result)).toBe(true);
expect(result.toISOString()).toBe(dateStr);
});
});

describe('convertStringToMomentOptional', () => {
it('should convert ISO string to Moment if value is provided', () => {
const dateStr = '2025-06-26T12:34:56.789Z';
const result = convertStringToMomentOptional(dateStr);
expect(moment.isMoment(result)).toBe(true);
expect(result?.toISOString()).toBe(dateStr);
});

it('should return undefined if value is not provided', () => {
const result = convertStringToMomentOptional(undefined);
expect(result).toBeUndefined();
});
});

describe('convertMomentToString', () => {
it('should convert Moment to ISO string', () => {
const m = moment('2025-06-26T12:34:56.789Z');
const result = convertMomentToString(m);
expect(result).toBe('2025-06-26T12:34:56.789Z');
});
});

describe('convertMomentToStringOptional', () => {
it('should convert Moment to ISO string if value is provided', () => {
const m = moment('2025-06-26T12:34:56.789Z');
const result = convertMomentToStringOptional(m);
expect(result).toBe('2025-06-26T12:34:56.789Z');
});

it('should return empty string if value is not provided', () => {
const result = convertMomentToStringOptional(undefined);
expect(result).toBe('');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { Moment } from 'moment';
import moment from 'moment';

export const convertStringToMoment = (value: string): Moment => moment(value);

export const convertStringToMomentOptional = (value?: string): Moment | undefined =>
value ? moment(value) : undefined;

export const convertMomentToString = (value: Moment): string => value?.toISOString();

export const convertMomentToStringOptional = (value?: Moment): string => value?.toISOString() ?? '';
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import { convertToRRule } from './convert_to_rrule';
describe('convertToRRule', () => {
const timezone = 'UTC';
const today = '2023-03-22';
const startDate = moment(today);
const startDate = moment(today).toISOString();

test('should convert a recurring schedule that is not recurring', () => {
const rRule = convertToRRule({ startDate, timezone });

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.YEARLY,
count: 1,
Expand All @@ -39,7 +39,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.DAILY,
interval: 1,
Expand All @@ -61,7 +61,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.DAILY,
interval: 1,
Expand All @@ -83,7 +83,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.DAILY,
interval: 1,
Expand All @@ -103,7 +103,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.WEEKLY,
interval: 1,
Expand All @@ -122,7 +122,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.MONTHLY,
interval: 1,
Expand All @@ -141,7 +141,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.YEARLY,
interval: 1,
Expand All @@ -163,7 +163,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.DAILY,
interval: 1,
Expand All @@ -184,7 +184,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.WEEKLY,
interval: 1,
Expand All @@ -206,7 +206,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.MONTHLY,
interval: 1,
Expand All @@ -228,7 +228,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.MONTHLY,
interval: 1,
Expand All @@ -249,7 +249,7 @@ describe('convertToRRule', () => {
});

expect(rRule).toEqual({
dtstart: startDate.toISOString(),
dtstart: startDate,
tzid: 'UTC',
freq: Frequency.YEARLY,
interval: 3,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { Moment } from 'moment';
import moment from 'moment';
import { Frequency } from '@kbn/rrule';
import { ISO_WEEKDAYS_TO_RRULE } from '../constants';
import { getPresets } from './get_presets';
Expand All @@ -21,20 +21,21 @@ export const convertToRRule = ({
recurringSchedule,
includeTime = false,
}: {
startDate: Moment;
startDate: string;
timezone: string;
recurringSchedule?: RecurringSchedule;
includeTime?: boolean;
}): RRuleParams => {
const presets = getPresets(startDate);
const startDateMoment = moment(startDate);
const presets = getPresets(startDateMoment);

const parsedSchedule = parseSchedule(recurringSchedule);

const rRule: RRuleParams = {
dtstart: startDate.toISOString(),
dtstart: startDateMoment.toISOString(),
tzid: timezone,
...(Boolean(includeTime)
? { byhour: [startDate.get('hour')], byminute: [startDate.get('minute')] }
? { byhour: [startDateMoment.get('hour')], byminute: [startDateMoment.get('minute')] }
: {}),
};

Expand Down Expand Up @@ -74,16 +75,16 @@ export const convertToRRule = ({

if (form.bymonth) {
if (form.bymonth === 'day') {
rRule.bymonthday = [startDate.date()];
rRule.bymonthday = [startDateMoment.date()];
} else if (form.bymonth === 'weekday') {
rRule.byweekday = [getNthByWeekday(startDate)];
rRule.byweekday = [getNthByWeekday(startDateMoment)];
}
}

if (frequency === Frequency.YEARLY) {
// rRule expects 1 based indexing for months
rRule.bymonth = [startDate.month() + 1];
rRule.bymonthday = [startDate.date()];
rRule.bymonth = [startDateMoment.month() + 1];
rRule.bymonthday = [startDateMoment.date()];
}

return rRule;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@
*/

import React, { PropsWithChildren } from 'react';
import moment from 'moment';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { type ReportingAPIClient, useKibana } from '@kbn/reporting-public';
import { coreMock } from '@kbn/core/public/mocks';
import { ReportTypeData, ScheduledReport } from '../../types';
import { getReportingHealth } from '../apis/get_reporting_health';
import { coreMock } from '@kbn/core/public/mocks';
import { testQueryClient } from '../test_utils/test_query_client';
import { QueryClientProvider } from '@tanstack/react-query';
import { ScheduledReportFlyoutContent } from './scheduled_report_flyout_content';
import { scheduleReport } from '../apis/schedule_report';
import { ScheduledReportApiJSON } from '../../../server/types';
import userEvent from '@testing-library/user-event';
import * as useDefaultTimezoneModule from '../hooks/use_default_timezone';

// Mock Kibana hooks and context
jest.mock('@kbn/reporting-public', () => ({
Expand Down Expand Up @@ -139,6 +141,10 @@ const mockKibanaServices = {
getCurrent: jest.fn().mockResolvedValue({ user: { email: TEST_EMAIL } }),
},
};
const defaultTimezone = moment.tz.guess();
const timezoneSpy = jest
.spyOn(useDefaultTimezoneModule, 'useDefaultTimezone')
.mockReturnValue({ defaultTimezone, isBrowser: true });

describe('ScheduledReportFlyoutContent', () => {
beforeEach(() => {
Expand Down Expand Up @@ -379,4 +385,63 @@ describe('ScheduledReportFlyoutContent', () => {
expect(mockValidateEmailAddresses).toHaveBeenCalled();
expect(emailInput).not.toBeValid();
});

it('should use default values for startDate and timezone if not provided', async () => {
const systemTime = moment('2025-07-01');
jest.useFakeTimers().setSystemTime(systemTime.toDate());

render(
<TestProviders>
<ScheduledReportFlyoutContent
apiClient={mockApiClient}
objectType={objectType}
sharingData={sharingData}
scheduledReport={{ reportTypeId: 'printablePdfV2' }}
availableReportTypes={availableFormats}
onClose={mockOnClose}
/>
</TestProviders>
);

const timezoneField = await screen.findByTestId('timezoneCombobox');
expect(within(timezoneField).getByText(defaultTimezone)).toBeInTheDocument();

const startDatePicker = await screen.findByTestId('startDatePicker');
const startDateInput = within(startDatePicker).getByRole('textbox');
const startDateValue = startDateInput.getAttribute('value')!;
expect(startDateValue).toEqual(systemTime.format('MM/DD/YYYY hh:mm A'));

timezoneSpy.mockRestore();
jest.useRealTimers();
});

it('should show a validation error if startDate is in the past', async () => {
const systemTime = moment('2025-07-02');
jest.useFakeTimers().setSystemTime(systemTime.toDate());

render(
<TestProviders>
<ScheduledReportFlyoutContent
apiClient={mockApiClient}
objectType={objectType}
sharingData={sharingData}
scheduledReport={{
reportTypeId: 'printablePdfV2',
}}
availableReportTypes={availableFormats}
onClose={mockOnClose}
/>
</TestProviders>
);

const startDatePicker = await screen.findByTestId('startDatePicker');
const startDateInput = within(startDatePicker).getByRole('textbox');
fireEvent.change(startDateInput, { target: { value: '07/01/2025 10:00 AM' } });
fireEvent.blur(startDateInput);

expect(await screen.findByText('Start date must be in the future')).toBeInTheDocument();

timezoneSpy.mockRestore();
jest.useRealTimers();
});
});
Loading