diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts index f78a4af51f2..58804d45298 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts @@ -14,11 +14,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export const getPeriodOptions = () => { return [ - /* { - label: 'minute', + { + label: 'minutes', value: 'minute', - prep: '' - },*/ + prep: '', + }, { label: 'hour', value: 'hour', @@ -57,7 +57,7 @@ export const toDisplay = { }; export const combinations = { - minute: /^(\*\s){4}\*$/, // "* * * * *" + minute: /^(\*\/\d{1,2})\s(\*\s){3}\*$/, // "*/? * * * *" hour: /^\d{1,2}\s(\*\s){3}\*$/, // "? * * * *" day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *" week: /^(\d{1,2}\s){2}(\*\s){2}\d{1,2}$/, // "? ? * * ?" @@ -78,6 +78,10 @@ export const getRangeOptions = (n: number) => { }); }; +export const getMinuteSegmentOptions = () => { + return getRangeOptions(60).filter((v) => v.value !== 0 && v.value % 5 === 0); +}; + export const getMinuteOptions = () => { return getRangeOptions(60); }; @@ -161,8 +165,8 @@ export const getMonthOptions = () => { }); }; -export const getMinuteCron = () => { - return '* * * * *'; +export const getMinuteCron = (value: any) => { + return `*/${value.min} * * * *`; }; export const getHourCron = (value: any) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.jsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.jsx index 06d5766cdce..8aa61f7935a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.jsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.jsx @@ -14,6 +14,7 @@ /* eslint-disable */ import React, { useState } from 'react'; +import { pluralize } from '../../../utils/CommonUtils'; import { combinations, getDayCron, @@ -22,6 +23,7 @@ import { getHourOptions, getMinuteCron, getMinuteOptions, + getMinuteSegmentOptions, getMonthCron, getMonthDaysOptions, getMonthOptions, @@ -34,6 +36,7 @@ import { const getCron = (state) => { const { selectedPeriod, + selectedMinOption, selectedHourOption, selectedDayOption, selectedWeekOption, @@ -43,7 +46,7 @@ const getCron = (state) => { switch (selectedPeriod) { case 'minute': - return getMinuteCron({}); + return getMinuteCron(selectedMinOption); case 'hour': return getHourCron(selectedHourOption); case 'day': @@ -72,6 +75,9 @@ const CronEditor = (props) => { const getStateValue = (valueStr) => { let stateVal = { selectedPeriod: 'week', + selectedMinOption: { + min: 5, + }, selectedHourOption: { min: 0, }, @@ -131,14 +137,13 @@ const CronEditor = (props) => { }; const [value, setCronValue] = useState(props.value || '0 0 * * 0'); const [state, setState] = useState(getStateValue(value)); - const [periodOptions, setPeriodOptions] = useState(getPeriodOptions()); - const [minuteOptions, setMinuteOptions] = useState(getMinuteOptions()); - const [hourOptions, setHourOptions] = useState(getHourOptions()); - const [dayOptions, setDayOptions] = useState(getDayOptions()); - const [monthDaysOptions, setMonthDaysOptions] = useState( - getMonthDaysOptions() - ); - const [monthOptions, setMonthOptions] = useState(getMonthOptions()); + const [periodOptions] = useState(getPeriodOptions()); + const [minuteSegmentOptions] = useState(getMinuteSegmentOptions()); + const [minuteOptions] = useState(getMinuteOptions()); + const [hourOptions] = useState(getHourOptions()); + const [dayOptions] = useState(getDayOptions()); + const [monthDaysOptions] = useState(getMonthDaysOptions()); + const [monthOptions] = useState(getMonthOptions()); const { className, disabled } = props; const { selectedPeriod } = state; @@ -171,6 +176,17 @@ const CronEditor = (props) => { setState((prev) => ({ ...prev, selectedHourOption: hourOption })); }; + const onMinOptionSelect = (event, key) => { + const selectedValue = event.target.value; + const obj = {}; + + obj[key] = selectedValue; + const { selectedMinOption } = state; + const minOption = Object.assign({}, selectedMinOption, obj); + changeValue({ ...state, selectedMinOption: minOption }); + setState((prev) => ({ ...prev, selectedMinOption: minOption })); + }; + const onDayOptionSelect = (event, key) => { const value = event.target.value; const obj = {}; @@ -247,6 +263,7 @@ const CronEditor = (props) => { return ( { @@ -273,6 +291,23 @@ const CronEditor = (props) => { ); }; + + const getMinuteSegmentSelect = (selectedOption, onChangeCB) => { + return ( + + ); + }; + const getBadgeOptions = (options, value, substrVal, onClick) => { const { disabled } = props; const optionComps = []; @@ -302,11 +337,22 @@ const CronEditor = (props) => { }; const getMinuteComponent = (cronPeriodString) => { + const { selectedMinOption } = state; return ( state.selectedPeriod === 'minute' && ( - - {getTextComp(`${cronPeriodString}`)} - + <> +
+ + {getMinuteSegmentSelect(selectedMinOption, (e) => + onMinOptionSelect(e, 'min') + )} +
+
+ {getTextComp( + `${cronPeriodString} ${selectedMinOption.min} minutes` + )} +
+ ) ); }; @@ -317,7 +363,7 @@ const CronEditor = (props) => { return ( state.selectedPeriod === 'hour' && ( <> -
+
{getMinuteSelect(selectedHourOption, (e) => onHourOptionSelect(e, 'min') @@ -325,7 +371,10 @@ const CronEditor = (props) => {
{getTextComp( - `${cronPeriodString} ${selectedHourOption.min} minute past the hour` + `${cronPeriodString} ${pluralize( + +selectedHourOption.min, + 'minute' + )} past the hour` )}
@@ -342,9 +391,9 @@ const CronEditor = (props) => { return ( state.selectedPeriod === 'day' && ( <> -
+
-
+
{getHourSelect(selectedDayOption, (e) => onDayOptionSelect(e, 'hour') )} @@ -375,9 +424,11 @@ const CronEditor = (props) => { return ( state.selectedPeriod === 'week' && ( <> -
+
-
+
{getHourSelect(selectedWeekOption, (e) => onWeekOptionSelect(e, 'hour') )} @@ -387,7 +438,9 @@ const CronEditor = (props) => { )}
-
+
Day :
{getBadgeOptions(dayOptions, selectedWeekOption.dow, 1, (e) => @@ -501,16 +554,17 @@ const CronEditor = (props) => { }; return ( -
+
-
+
- {getMinuteComponent(cronPeriodString)} + {getMinuteComponent(startText)} {getHourComponent(cronPeriodString)} {getDayComponent(cronPeriodString)} {getWeekComponent(cronPeriodString)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.test.tsx new file mode 100644 index 00000000000..7b5d785f0b8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright 2021 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import CronEditor from './CronEditor'; + +describe('Test CronEditor component', () => { + it('CronEditor component should render', async () => { + render(); + + expect(await screen.findByTestId('cron-container')).toBeInTheDocument(); + expect( + await screen.findByTestId('time-dropdown-container') + ).toBeInTheDocument(); + expect(await screen.findByTestId('ingestion-type')).toBeInTheDocument(); + }); + + it('Hour option should render corrosponding component', async () => { + render(); + + const ingestionType = await screen.findByTestId('ingestion-type'); + userEvent.selectOptions(ingestionType, 'hour'); + + expect( + await screen.findByTestId('hour-segment-container') + ).toBeInTheDocument(); + + const minutOptions = await screen.findByTestId('minute-options'); + + expect(minutOptions).toBeInTheDocument(); + + userEvent.selectOptions(minutOptions, '10'); + + expect(await screen.findByText('10')).toBeInTheDocument(); + }); + + it('Minute option should render corrosponding component', async () => { + render(); + + const ingestionType = await screen.findByTestId('ingestion-type'); + userEvent.selectOptions(ingestionType, 'minute'); + + expect( + await screen.findByTestId('minute-segment-container') + ).toBeInTheDocument(); + + const minutOptions = await screen.findByTestId('minute-segment-options'); + + expect(minutOptions).toBeInTheDocument(); + + userEvent.selectOptions(minutOptions, '10'); + + expect(await screen.findByText('10')).toBeInTheDocument(); + }); + + it('Day option should render corrosponding component', async () => { + render(); + + const ingestionType = await screen.findByTestId('ingestion-type'); + userEvent.selectOptions(ingestionType, 'day'); + + expect( + await screen.findByTestId('day-segment-container') + ).toBeInTheDocument(); + expect( + await screen.findByTestId('time-option-container') + ).toBeInTheDocument(); + + const minutOptions = await screen.findByTestId('minute-options'); + const hourOptions = await screen.findByTestId('hour-options'); + + expect(minutOptions).toBeInTheDocument(); + expect(hourOptions).toBeInTheDocument(); + + userEvent.selectOptions(minutOptions, '10'); + userEvent.selectOptions(hourOptions, '2'); + + expect((await screen.findAllByText('10')).length).toBe(2); + expect((await screen.findAllByText('02')).length).toBe(2); + }); + + it('week option should render corrosponding component', async () => { + render(); + + const ingestionType = await screen.findByTestId('ingestion-type'); + userEvent.selectOptions(ingestionType, 'week'); + + expect( + await screen.findByTestId('week-segment-time-container') + ).toBeInTheDocument(); + expect( + await screen.findByTestId('week-segment-time-options-container') + ).toBeInTheDocument(); + expect( + await screen.findByTestId('week-segment-day-option-container') + ).toBeInTheDocument(); + + const minutOptions = await screen.findByTestId('minute-options'); + const hourOptions = await screen.findByTestId('hour-options'); + + expect(minutOptions).toBeInTheDocument(); + expect(hourOptions).toBeInTheDocument(); + + userEvent.selectOptions(minutOptions, '10'); + userEvent.selectOptions(hourOptions, '2'); + + expect((await screen.findAllByText('10')).length).toBe(2); + expect((await screen.findAllByText('02')).length).toBe(2); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 004b9186bc3..56040de0476 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -159,7 +159,7 @@ export const pluralize = (count: number, noun: string, suffix = 's') => { count > 1 ? noun : noun.slice(0, noun.length - 1) }`; } else { - return `${countString} ${noun}${count !== 1 ? suffix : ''}`; + return `${countString} ${noun}${count > 1 ? suffix : ''}`; } } };