[ResponseOps][MaintenanceWindows] Fix startDate not forwarded to custom recurrence component (#223949)

## Summary

- Forwards `startDate` correctly to the `CustomRecurringSchedule`
component. The missing prop caused the monthly custom frequency
sub-options to not show up.
- Fixes the `CustomRecurringSchedule` component type to correctly
reflect the required prop.
- Removes the `custom-recurring-form` data test subject from the
`<CustomRecurringSchedule>` JSX tag. The test subject wasn't forwarded
to any DOM element, but the only test with an assertion using that test
subject was passing because it was checking its absence
(`not.toBeInTheDocument()`).

## Verification steps

1. Open the Maintenance Window creation page
2. Toggle "Repeat" on
3. In the recurrence form, check that all the custom frequencies work,
showing the correct sub-options
This commit is contained in:
Umberto Pepato 2025-06-16 14:37:01 +02:00 committed by GitHub
parent d017a33f99
commit 73a2469bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 156 additions and 131 deletions

View file

@ -37,6 +37,8 @@ const TestWrapper = ({ children, iv = initialValue }: PropsWithChildren<{ iv?: F
return <Form form={form}>{children}</Form>; return <Form form={form}>{children}</Form>;
}; };
const startDate = new Date().toISOString();
describe('CustomRecurringSchedule', () => { describe('CustomRecurringSchedule', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -45,7 +47,7 @@ describe('CustomRecurringSchedule', () => {
it('renders all form fields', async () => { it('renders all form fields', async () => {
render( render(
<TestWrapper> <TestWrapper>
<CustomRecurringSchedule /> <CustomRecurringSchedule startDate={startDate} />
</TestWrapper> </TestWrapper>
); );
@ -58,7 +60,7 @@ describe('CustomRecurringSchedule', () => {
it('renders byweekday field if custom frequency = weekly', async () => { it('renders byweekday field if custom frequency = weekly', async () => {
render( render(
<TestWrapper> <TestWrapper>
<CustomRecurringSchedule /> <CustomRecurringSchedule startDate={startDate} />
</TestWrapper> </TestWrapper>
); );
@ -83,7 +85,7 @@ describe('CustomRecurringSchedule', () => {
}; };
render( render(
<TestWrapper iv={iv}> <TestWrapper iv={iv}>
<CustomRecurringSchedule /> <CustomRecurringSchedule startDate={startDate} />
</TestWrapper> </TestWrapper>
); );
@ -93,7 +95,7 @@ describe('CustomRecurringSchedule', () => {
it('renders bymonth field if custom frequency = monthly', async () => { it('renders bymonth field if custom frequency = monthly', async () => {
render( render(
<TestWrapper> <TestWrapper>
<CustomRecurringSchedule /> <CustomRecurringSchedule startDate={startDate} />
</TestWrapper> </TestWrapper>
); );
@ -111,7 +113,7 @@ describe('CustomRecurringSchedule', () => {
it('should initialize the form when no initialValue provided', () => { it('should initialize the form when no initialValue provided', () => {
render( render(
<TestWrapper> <TestWrapper>
<CustomRecurringSchedule /> <CustomRecurringSchedule startDate={startDate} />
</TestWrapper> </TestWrapper>
); );
@ -139,7 +141,7 @@ describe('CustomRecurringSchedule', () => {
}; };
render( render(
<TestWrapper iv={iv}> <TestWrapper iv={iv}>
<CustomRecurringSchedule /> <CustomRecurringSchedule startDate={startDate} />
</TestWrapper> </TestWrapper>
); );

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import React, { useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { Frequency } from '@kbn/rrule'; import { Frequency } from '@kbn/rrule';
import moment from 'moment'; import moment from 'moment';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
@ -42,136 +42,134 @@ const styles = {
}; };
export interface CustomRecurringScheduleProps { export interface CustomRecurringScheduleProps {
startDate?: string; startDate: string;
} }
export const CustomRecurringSchedule: React.FC = React.memo( export const CustomRecurringSchedule = memo(({ startDate }: CustomRecurringScheduleProps) => {
({ startDate }: CustomRecurringScheduleProps) => { const [{ recurringSchedule }] = useFormData<{ recurringSchedule: RecurringSchedule }>({
const [{ recurringSchedule }] = useFormData<{ recurringSchedule: RecurringSchedule }>({ watch: [
watch: [ 'recurringSchedule.frequency',
'recurringSchedule.frequency', 'recurringSchedule.interval',
'recurringSchedule.interval', 'recurringSchedule.customFrequency',
'recurringSchedule.customFrequency', ],
], });
});
const parsedSchedule = useMemo(() => { const parsedSchedule = useMemo(() => {
return parseSchedule(recurringSchedule); return parseSchedule(recurringSchedule);
}, [recurringSchedule]); }, [recurringSchedule]);
const frequencyOptions = useMemo( const frequencyOptions = useMemo(
() => RECURRING_SCHEDULE_FORM_CUSTOM_FREQUENCY(parsedSchedule?.interval), () => RECURRING_SCHEDULE_FORM_CUSTOM_FREQUENCY(parsedSchedule?.interval),
[parsedSchedule?.interval] [parsedSchedule?.interval]
); );
const bymonthOptions = useMemo(() => { const bymonthOptions = useMemo(() => {
if (!startDate) return []; if (!startDate) return [];
const date = moment(startDate); const date = moment(startDate);
const { dayOfWeek, nthWeekdayOfMonth, isLastOfMonth } = getWeekdayInfo(date, 'ddd'); const { dayOfWeek, nthWeekdayOfMonth, isLastOfMonth } = getWeekdayInfo(date, 'ddd');
return [ return [
{ {
id: 'day', id: 'day',
label: RECURRING_SCHEDULE_FORM_CUSTOM_REPEAT_MONTHLY_ON_DAY(date), label: RECURRING_SCHEDULE_FORM_CUSTOM_REPEAT_MONTHLY_ON_DAY(date),
}, },
{ {
id: 'weekday', id: 'weekday',
label: label:
RECURRING_SCHEDULE_FORM_WEEKDAY_SHORT(dayOfWeek)[isLastOfMonth ? 0 : nthWeekdayOfMonth], RECURRING_SCHEDULE_FORM_WEEKDAY_SHORT(dayOfWeek)[isLastOfMonth ? 0 : nthWeekdayOfMonth],
}, },
]; ];
}, [startDate]); }, [startDate]);
const defaultByWeekday = useMemo(() => getInitialByWeekday([], moment(startDate)), [startDate]); const defaultByWeekday = useMemo(() => getInitialByWeekday([], moment(startDate)), [startDate]);
return ( return (
<> <>
{parsedSchedule?.frequency !== Frequency.DAILY ? ( {parsedSchedule?.frequency !== Frequency.DAILY ? (
<> <>
<EuiSpacer size="s" /> <EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" alignItems="flexStart"> <EuiFlexGroup gutterSize="s" alignItems="flexStart">
<EuiFlexItem> <EuiFlexItem>
<UseField <UseField
path="recurringSchedule.interval" path="recurringSchedule.interval"
css={styles.flexField} css={styles.flexField}
componentProps={{ componentProps={{
'data-test-subj': 'interval-field', 'data-test-subj': 'interval-field',
id: 'interval', id: 'interval',
euiFieldProps: { euiFieldProps: {
'data-test-subj': 'customRecurringScheduleIntervalInput', 'data-test-subj': 'customRecurringScheduleIntervalInput',
min: 1, min: 1,
prepend: ( prepend: (
<EuiFormLabel htmlFor={'interval'}> <EuiFormLabel htmlFor={'interval'}>
{RECURRING_SCHEDULE_FORM_INTERVAL_EVERY} {RECURRING_SCHEDULE_FORM_INTERVAL_EVERY}
</EuiFormLabel> </EuiFormLabel>
), ),
},
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="recurringSchedule.customFrequency"
componentProps={{
'data-test-subj': 'custom-frequency-field',
euiFieldProps: {
'data-test-subj': 'customRecurringScheduleFrequencySelect',
options: frequencyOptions,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
) : null}
{Number(parsedSchedule?.customFrequency) === Frequency.WEEKLY ||
parsedSchedule?.frequency === Frequency.DAILY ? (
<UseField
path="recurringSchedule.byweekday"
config={{
type: FIELD_TYPES.MULTI_BUTTON_GROUP,
label: '',
validations: [
{
validator: ({ value }) => {
if (
Object.values(value as MultiButtonGroupFieldValue).every((v) => v === false)
) {
return {
message: RECURRING_SCHEDULE_FORM_BYWEEKDAY_REQUIRED,
};
}
}, },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="recurringSchedule.customFrequency"
componentProps={{
'data-test-subj': 'custom-frequency-field',
euiFieldProps: {
'data-test-subj': 'customRecurringScheduleFrequencySelect',
options: frequencyOptions,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
) : null}
{Number(parsedSchedule?.customFrequency) === Frequency.WEEKLY ||
parsedSchedule?.frequency === Frequency.DAILY ? (
<UseField
path="recurringSchedule.byweekday"
config={{
type: FIELD_TYPES.MULTI_BUTTON_GROUP,
label: '',
validations: [
{
validator: ({ value }) => {
if (
Object.values(value as MultiButtonGroupFieldValue).every((v) => v === false)
) {
return {
message: RECURRING_SCHEDULE_FORM_BYWEEKDAY_REQUIRED,
};
}
}, },
],
defaultValue: defaultByWeekday,
}}
componentProps={{
'data-test-subj': 'byweekday-field',
euiFieldProps: {
'data-test-subj': 'customRecurringScheduleByWeekdayButtonGroup',
legend: 'Repeat on weekday',
options: WEEKDAY_OPTIONS,
}, },
}} ],
/> defaultValue: defaultByWeekday,
) : null} }}
componentProps={{
'data-test-subj': 'byweekday-field',
euiFieldProps: {
'data-test-subj': 'customRecurringScheduleByWeekdayButtonGroup',
legend: 'Repeat on weekday',
options: WEEKDAY_OPTIONS,
},
}}
/>
) : null}
{Number(parsedSchedule?.customFrequency) === Frequency.MONTHLY ? ( {Number(parsedSchedule?.customFrequency) === Frequency.MONTHLY ? (
<UseField <UseField
path="recurringSchedule.bymonth" path="recurringSchedule.bymonth"
componentProps={{ componentProps={{
'data-test-subj': 'bymonth-field', 'data-test-subj': 'bymonth-field',
euiFieldProps: { euiFieldProps: {
legend: 'Repeat on weekday or month day', legend: 'Repeat on weekday or month day',
options: bymonthOptions, options: bymonthOptions,
}, },
}} }}
/> />
) : null} ) : null}
</> </>
); );
} });
);
CustomRecurringSchedule.displayName = 'CustomRecurringSchedule'; CustomRecurringSchedule.displayName = 'CustomRecurringSchedule';

View file

@ -18,6 +18,7 @@ import type { RecurringSchedule } from '../types';
import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { getRecurringScheduleFormSchema } from '../schemas/recurring_schedule_form_schema'; import { getRecurringScheduleFormSchema } from '../schemas/recurring_schedule_form_schema';
import { RecurrenceEnd } from '../constants'; import { RecurrenceEnd } from '../constants';
import { Frequency } from '@kbn/rrule';
const baseProps: RecurringScheduleFieldsProps = { const baseProps: RecurringScheduleFieldsProps = {
startDate: '2023-03-24', startDate: '2023-03-24',
@ -29,7 +30,7 @@ interface FormValue {
const initialValue: FormValue = { const initialValue: FormValue = {
recurringSchedule: { recurringSchedule: {
frequency: 'CUSTOM', frequency: Frequency.WEEKLY,
ends: RecurrenceEnd.NEVER, ends: RecurrenceEnd.NEVER,
}, },
}; };
@ -57,7 +58,7 @@ describe('RecurringScheduleForm', () => {
); );
expect(screen.getByTestId('frequency-field')).toBeInTheDocument(); expect(screen.getByTestId('frequency-field')).toBeInTheDocument();
expect(screen.queryByTestId('custom-recurring-form')).not.toBeInTheDocument(); expect(screen.queryByTestId('customRecurringScheduleIntervalInput')).not.toBeInTheDocument();
expect(screen.getByTestId('ends-field')).toBeInTheDocument(); expect(screen.getByTestId('ends-field')).toBeInTheDocument();
expect(screen.queryByTestId('until-field')).not.toBeInTheDocument(); expect(screen.queryByTestId('until-field')).not.toBeInTheDocument();
expect(screen.queryByTestId('count-field')).not.toBeInTheDocument(); expect(screen.queryByTestId('count-field')).not.toBeInTheDocument();
@ -88,4 +89,28 @@ describe('RecurringScheduleForm', () => {
await userEvent.click(btn); await userEvent.click(btn);
expect(await screen.findByTestId('count-field')).toBeInTheDocument(); expect(await screen.findByTestId('count-field')).toBeInTheDocument();
}); });
it('renders custom schedule if frequency = daily', async () => {
render(
<TestWrapper
iv={{ recurringSchedule: { frequency: Frequency.DAILY, ends: RecurrenceEnd.NEVER } }}
>
<RecurringScheduleFormFields {...baseProps} />
</TestWrapper>
);
expect(
await screen.findByTestId('customRecurringScheduleByWeekdayButtonGroup')
).toBeInTheDocument();
});
it('renders custom schedule if frequency = custom', async () => {
render(
<TestWrapper iv={{ recurringSchedule: { frequency: 'CUSTOM', ends: RecurrenceEnd.NEVER } }}>
<RecurringScheduleFormFields {...baseProps} />
</TestWrapper>
);
expect(await screen.findByTestId('customRecurringScheduleIntervalInput')).toBeInTheDocument();
});
}); });

View file

@ -59,7 +59,7 @@ export const toMoment = (value: string): Moment => moment(value);
export const toString = (value: Moment): string => value.toISOString(); export const toString = (value: Moment): string => value.toISOString();
export interface RecurringScheduleFieldsProps { export interface RecurringScheduleFieldsProps {
startDate?: string; startDate: string;
endDate?: string; endDate?: string;
timezone?: string[]; timezone?: string[];
allowInfiniteRecurrence?: boolean; allowInfiniteRecurrence?: boolean;
@ -147,7 +147,7 @@ export const RecurringScheduleFormFields = memo(
/> />
{(parsedSchedule?.frequency === Frequency.DAILY || {(parsedSchedule?.frequency === Frequency.DAILY ||
parsedSchedule?.frequency === 'CUSTOM') && ( parsedSchedule?.frequency === 'CUSTOM') && (
<CustomRecurringSchedule data-test-subj="custom-recurring-form" /> <CustomRecurringSchedule startDate={startDate} />
)} )}
<UseField <UseField
path="recurringSchedule.ends" path="recurringSchedule.ends"