mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-01 05:03:10 +00:00
Added support for minute level selection for ingestion scheduler (#4994)
* Added support for minute level selection for ingestion scheduler * fix sonarCloude * addressing comment * fixing failing test
This commit is contained in:
parent
bcdae71eb0
commit
ee3f267e96
@ -14,11 +14,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
export const getPeriodOptions = () => {
|
export const getPeriodOptions = () => {
|
||||||
return [
|
return [
|
||||||
/* {
|
{
|
||||||
label: 'minute',
|
label: 'minutes',
|
||||||
value: 'minute',
|
value: 'minute',
|
||||||
prep: ''
|
prep: '',
|
||||||
},*/
|
},
|
||||||
{
|
{
|
||||||
label: 'hour',
|
label: 'hour',
|
||||||
value: 'hour',
|
value: 'hour',
|
||||||
@ -57,7 +57,7 @@ export const toDisplay = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const combinations = {
|
export const combinations = {
|
||||||
minute: /^(\*\s){4}\*$/, // "* * * * *"
|
minute: /^(\*\/\d{1,2})\s(\*\s){3}\*$/, // "*/? * * * *"
|
||||||
hour: /^\d{1,2}\s(\*\s){3}\*$/, // "? * * * *"
|
hour: /^\d{1,2}\s(\*\s){3}\*$/, // "? * * * *"
|
||||||
day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *"
|
day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *"
|
||||||
week: /^(\d{1,2}\s){2}(\*\s){2}\d{1,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 = () => {
|
export const getMinuteOptions = () => {
|
||||||
return getRangeOptions(60);
|
return getRangeOptions(60);
|
||||||
};
|
};
|
||||||
@ -161,8 +165,8 @@ export const getMonthOptions = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMinuteCron = () => {
|
export const getMinuteCron = (value: any) => {
|
||||||
return '* * * * *';
|
return `*/${value.min} * * * *`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHourCron = (value: any) => {
|
export const getHourCron = (value: any) => {
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { pluralize } from '../../../utils/CommonUtils';
|
||||||
import {
|
import {
|
||||||
combinations,
|
combinations,
|
||||||
getDayCron,
|
getDayCron,
|
||||||
@ -22,6 +23,7 @@ import {
|
|||||||
getHourOptions,
|
getHourOptions,
|
||||||
getMinuteCron,
|
getMinuteCron,
|
||||||
getMinuteOptions,
|
getMinuteOptions,
|
||||||
|
getMinuteSegmentOptions,
|
||||||
getMonthCron,
|
getMonthCron,
|
||||||
getMonthDaysOptions,
|
getMonthDaysOptions,
|
||||||
getMonthOptions,
|
getMonthOptions,
|
||||||
@ -34,6 +36,7 @@ import {
|
|||||||
const getCron = (state) => {
|
const getCron = (state) => {
|
||||||
const {
|
const {
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
|
selectedMinOption,
|
||||||
selectedHourOption,
|
selectedHourOption,
|
||||||
selectedDayOption,
|
selectedDayOption,
|
||||||
selectedWeekOption,
|
selectedWeekOption,
|
||||||
@ -43,7 +46,7 @@ const getCron = (state) => {
|
|||||||
|
|
||||||
switch (selectedPeriod) {
|
switch (selectedPeriod) {
|
||||||
case 'minute':
|
case 'minute':
|
||||||
return getMinuteCron({});
|
return getMinuteCron(selectedMinOption);
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return getHourCron(selectedHourOption);
|
return getHourCron(selectedHourOption);
|
||||||
case 'day':
|
case 'day':
|
||||||
@ -72,6 +75,9 @@ const CronEditor = (props) => {
|
|||||||
const getStateValue = (valueStr) => {
|
const getStateValue = (valueStr) => {
|
||||||
let stateVal = {
|
let stateVal = {
|
||||||
selectedPeriod: 'week',
|
selectedPeriod: 'week',
|
||||||
|
selectedMinOption: {
|
||||||
|
min: 5,
|
||||||
|
},
|
||||||
selectedHourOption: {
|
selectedHourOption: {
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
@ -131,14 +137,13 @@ const CronEditor = (props) => {
|
|||||||
};
|
};
|
||||||
const [value, setCronValue] = useState(props.value || '0 0 * * 0');
|
const [value, setCronValue] = useState(props.value || '0 0 * * 0');
|
||||||
const [state, setState] = useState(getStateValue(value));
|
const [state, setState] = useState(getStateValue(value));
|
||||||
const [periodOptions, setPeriodOptions] = useState(getPeriodOptions());
|
const [periodOptions] = useState(getPeriodOptions());
|
||||||
const [minuteOptions, setMinuteOptions] = useState(getMinuteOptions());
|
const [minuteSegmentOptions] = useState(getMinuteSegmentOptions());
|
||||||
const [hourOptions, setHourOptions] = useState(getHourOptions());
|
const [minuteOptions] = useState(getMinuteOptions());
|
||||||
const [dayOptions, setDayOptions] = useState(getDayOptions());
|
const [hourOptions] = useState(getHourOptions());
|
||||||
const [monthDaysOptions, setMonthDaysOptions] = useState(
|
const [dayOptions] = useState(getDayOptions());
|
||||||
getMonthDaysOptions()
|
const [monthDaysOptions] = useState(getMonthDaysOptions());
|
||||||
);
|
const [monthOptions] = useState(getMonthOptions());
|
||||||
const [monthOptions, setMonthOptions] = useState(getMonthOptions());
|
|
||||||
|
|
||||||
const { className, disabled } = props;
|
const { className, disabled } = props;
|
||||||
const { selectedPeriod } = state;
|
const { selectedPeriod } = state;
|
||||||
@ -171,6 +176,17 @@ const CronEditor = (props) => {
|
|||||||
setState((prev) => ({ ...prev, selectedHourOption: hourOption }));
|
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 onDayOptionSelect = (event, key) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
const obj = {};
|
const obj = {};
|
||||||
@ -247,6 +263,7 @@ const CronEditor = (props) => {
|
|||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className="tw-form-inputs tw-py-1 tw-px-1"
|
className="tw-form-inputs tw-py-1 tw-px-1"
|
||||||
|
data-testid="hour-options"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={selectedOption.hour}
|
value={selectedOption.hour}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -263,6 +280,7 @@ const CronEditor = (props) => {
|
|||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className="tw-form-inputs tw-py-1 tw-px-1"
|
className="tw-form-inputs tw-py-1 tw-px-1"
|
||||||
|
data-testid="minute-options"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={selectedOption.min}
|
value={selectedOption.min}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -273,6 +291,23 @@ const CronEditor = (props) => {
|
|||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMinuteSegmentSelect = (selectedOption, onChangeCB) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="tw-form-inputs tw-py-1 tw-px-1"
|
||||||
|
data-testid="minute-segment-options"
|
||||||
|
disabled={props.disabled}
|
||||||
|
value={selectedOption.min}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.persist();
|
||||||
|
onChangeCB(e);
|
||||||
|
}}>
|
||||||
|
{minuteSegmentOptions.map(getOptionComponent('minute_option'))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getBadgeOptions = (options, value, substrVal, onClick) => {
|
const getBadgeOptions = (options, value, substrVal, onClick) => {
|
||||||
const { disabled } = props;
|
const { disabled } = props;
|
||||||
const optionComps = [];
|
const optionComps = [];
|
||||||
@ -302,11 +337,22 @@ const CronEditor = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getMinuteComponent = (cronPeriodString) => {
|
const getMinuteComponent = (cronPeriodString) => {
|
||||||
|
const { selectedMinOption } = state;
|
||||||
return (
|
return (
|
||||||
state.selectedPeriod === 'minute' && (
|
state.selectedPeriod === 'minute' && (
|
||||||
<cron-minute-component>
|
<>
|
||||||
{getTextComp(`${cronPeriodString}`)}
|
<div className="tw-mb-1.5" data-testid="minute-segment-container">
|
||||||
</cron-minute-component>
|
<label>Minute :</label>
|
||||||
|
{getMinuteSegmentSelect(selectedMinOption, (e) =>
|
||||||
|
onMinOptionSelect(e, 'min')
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="tw-col-span-2">
|
||||||
|
{getTextComp(
|
||||||
|
`${cronPeriodString} ${selectedMinOption.min} minutes`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -317,7 +363,7 @@ const CronEditor = (props) => {
|
|||||||
return (
|
return (
|
||||||
state.selectedPeriod === 'hour' && (
|
state.selectedPeriod === 'hour' && (
|
||||||
<>
|
<>
|
||||||
<div className="tw-mb-1.5">
|
<div className="tw-mb-1.5" data-testid="hour-segment-container">
|
||||||
<label>Minute :</label>
|
<label>Minute :</label>
|
||||||
{getMinuteSelect(selectedHourOption, (e) =>
|
{getMinuteSelect(selectedHourOption, (e) =>
|
||||||
onHourOptionSelect(e, 'min')
|
onHourOptionSelect(e, 'min')
|
||||||
@ -325,7 +371,10 @@ const CronEditor = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="tw-col-span-2">
|
<div className="tw-col-span-2">
|
||||||
{getTextComp(
|
{getTextComp(
|
||||||
`${cronPeriodString} ${selectedHourOption.min} minute past the hour`
|
`${cronPeriodString} ${pluralize(
|
||||||
|
+selectedHourOption.min,
|
||||||
|
'minute'
|
||||||
|
)} past the hour`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -342,9 +391,9 @@ const CronEditor = (props) => {
|
|||||||
return (
|
return (
|
||||||
state.selectedPeriod === 'day' && (
|
state.selectedPeriod === 'day' && (
|
||||||
<>
|
<>
|
||||||
<div className="tw-mb-1.5">
|
<div className="tw-mb-1.5" data-testid="day-segment-container">
|
||||||
<label>Time :</label>
|
<label>Time :</label>
|
||||||
<div className="tw-flex">
|
<div className="tw-flex" data-testid="time-option-container">
|
||||||
{getHourSelect(selectedDayOption, (e) =>
|
{getHourSelect(selectedDayOption, (e) =>
|
||||||
onDayOptionSelect(e, 'hour')
|
onDayOptionSelect(e, 'hour')
|
||||||
)}
|
)}
|
||||||
@ -375,9 +424,11 @@ const CronEditor = (props) => {
|
|||||||
return (
|
return (
|
||||||
state.selectedPeriod === 'week' && (
|
state.selectedPeriod === 'week' && (
|
||||||
<>
|
<>
|
||||||
<div className="tw-mb-1.5">
|
<div className="tw-mb-1.5" data-testid="week-segment-time-container">
|
||||||
<label>Time :</label>
|
<label>Time :</label>
|
||||||
<div className="tw-flex">
|
<div
|
||||||
|
className="tw-flex"
|
||||||
|
data-testid="week-segment-time-options-container">
|
||||||
{getHourSelect(selectedWeekOption, (e) =>
|
{getHourSelect(selectedWeekOption, (e) =>
|
||||||
onWeekOptionSelect(e, 'hour')
|
onWeekOptionSelect(e, 'hour')
|
||||||
)}
|
)}
|
||||||
@ -387,7 +438,9 @@ const CronEditor = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tw-pt-2">
|
<div
|
||||||
|
className="tw-pt-2"
|
||||||
|
data-testid="week-segment-day-option-container">
|
||||||
<span>Day : </span>
|
<span>Day : </span>
|
||||||
<div className="cron-badge-option-container week-opt-container">
|
<div className="cron-badge-option-container week-opt-container">
|
||||||
{getBadgeOptions(dayOptions, selectedWeekOption.dow, 1, (e) =>
|
{getBadgeOptions(dayOptions, selectedWeekOption.dow, 1, (e) =>
|
||||||
@ -501,16 +554,17 @@ const CronEditor = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${className} cron-row`}>
|
<div className={`${className} cron-row`} data-testid="cron-container">
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="tw-grid tw-grid-cols-2 tw-gap-4">
|
<div className="tw-grid tw-grid-cols-2 tw-gap-4">
|
||||||
<div className="tw-mb-1.5">
|
<div className="tw-mb-1.5" data-testid="time-dropdown-container">
|
||||||
<label htmlFor="ingestionType">Every:</label>
|
<label htmlFor="ingestionType">Every:</label>
|
||||||
<select
|
<select
|
||||||
className="tw-form-inputs tw-px-3 tw-py-1"
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id="ingestionType"
|
id="ingestionType"
|
||||||
name="ingestionType"
|
name="ingestionType"
|
||||||
|
data-testid="ingestion-type"
|
||||||
value={selectedPeriod}
|
value={selectedPeriod}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.persist();
|
e.persist();
|
||||||
@ -526,7 +580,7 @@ const CronEditor = (props) => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getMinuteComponent(cronPeriodString)}
|
{getMinuteComponent(startText)}
|
||||||
{getHourComponent(cronPeriodString)}
|
{getHourComponent(cronPeriodString)}
|
||||||
{getDayComponent(cronPeriodString)}
|
{getDayComponent(cronPeriodString)}
|
||||||
{getWeekComponent(cronPeriodString)}
|
{getWeekComponent(cronPeriodString)}
|
||||||
|
@ -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(<CronEditor />);
|
||||||
|
|
||||||
|
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(<CronEditor disabled={false} onChange={jest.fn} />);
|
||||||
|
|
||||||
|
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(<CronEditor disabled={false} onChange={jest.fn} />);
|
||||||
|
|
||||||
|
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(<CronEditor disabled={false} onChange={jest.fn} />);
|
||||||
|
|
||||||
|
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(<CronEditor disabled={false} onChange={jest.fn} />);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -159,7 +159,7 @@ export const pluralize = (count: number, noun: string, suffix = 's') => {
|
|||||||
count > 1 ? noun : noun.slice(0, noun.length - 1)
|
count > 1 ? noun : noun.slice(0, noun.length - 1)
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else {
|
||||||
return `${countString} ${noun}${count !== 1 ? suffix : ''}`;
|
return `${countString} ${noun}${count > 1 ? suffix : ''}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user