diff --git a/frontend/src/concepts/pipelines/content/createRun/const.ts b/frontend/src/concepts/pipelines/content/createRun/const.ts index 5ea096ef5e..1e844be99d 100644 --- a/frontend/src/concepts/pipelines/content/createRun/const.ts +++ b/frontend/src/concepts/pipelines/content/createRun/const.ts @@ -1,7 +1,7 @@ import { PeriodicOptions } from '~/concepts/pipelines/content/createRun/types'; export const DEFAULT_CRON_STRING = '0 0 0 * * *'; -export const DEFAULT_PERIODIC_OPTION = PeriodicOptions.HOUR; +export const DEFAULT_PERIODIC_OPTION = PeriodicOptions.WEEK; export const DATE_FORMAT = 'YYYY-MM-DD'; export const DEFAULT_TIME = '12:00 AM'; diff --git a/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSectionScheduled.tsx b/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSectionScheduled.tsx index 61d9bb118e..8da70f8a10 100644 --- a/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSectionScheduled.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSectionScheduled.tsx @@ -1,5 +1,13 @@ import * as React from 'react'; -import { ClipboardCopy, Radio, Stack, StackItem, Text } from '@patternfly/react-core'; +import { + ClipboardCopy, + Radio, + Stack, + StackItem, + Split, + SplitItem, + Text, +} from '@patternfly/react-core'; import { PeriodicOptions, RunTypeScheduledData, @@ -12,6 +20,8 @@ import { DEFAULT_PERIODIC_OPTION, } from '~/concepts/pipelines/content/createRun/const'; import EndDateBeforeStartDateError from '~/concepts/pipelines/content/createRun/contentSections/EndDateBeforeStartDateError'; +import { replaceNonNumericPartWithString, replaceNumericPartWithString } from '~/utilities/string'; +import NumberInputWrapper from '~/components/NumberInputWrapper'; type RunTypeSectionScheduledProps = { data: RunTypeScheduledData; @@ -32,7 +42,11 @@ const RunTypeSectionScheduled: React.FC = ({ data, isChecked={data.triggerType === ScheduledType.PERIODIC} id={ScheduledType.PERIODIC} onChange={() => - onChange({ ...data, triggerType: ScheduledType.PERIODIC, value: DEFAULT_PERIODIC_OPTION }) + onChange({ + ...data, + triggerType: ScheduledType.PERIODIC, + value: DEFAULT_PERIODIC_OPTION, + }) } body={ data.triggerType === ScheduledType.PERIODIC && ( @@ -40,14 +54,35 @@ const RunTypeSectionScheduled: React.FC = ({ data, Run every - ({ - key: v, - label: v, - }))} - value={data.value} - onChange={(value) => onChange({ ...data, value })} - /> + + + + onChange({ + ...data, + value: replaceNumericPartWithString(data.value, value), + }) + } + /> + + + ({ + key: v, + label: v, + }))} + value={data.value.replace(/\d+/, '')} + onChange={(value) => + onChange({ + ...data, + value: replaceNonNumericPartWithString(data.value, value), + }) + } + /> + + ) } diff --git a/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts b/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts index fcc8337cb3..7a676d36c1 100644 --- a/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts +++ b/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts @@ -1,5 +1,4 @@ import { - periodicOptionAsSeconds, RunDateTime, RunFormData, RunTypeOption, @@ -16,6 +15,7 @@ import { } from '~/concepts/pipelines/kfTypes'; import { PipelineAPIs } from '~/concepts/pipelines/types'; import { isFilledRunFormData } from '~/concepts/pipelines/content/createRun/utils'; +import { convertPeriodicTimeToSeconds } from '~/utilities/time'; const getResourceReferences = (formData: SafeRunFormData): ResourceReferenceKF[] => { const refs: ResourceReferenceKF[] = []; @@ -79,6 +79,7 @@ const createJob = async ( const startDate = convertDateDataToKFDateTime(formData.runType.data.start) ?? undefined; const endDate = convertDateDataToKFDateTime(formData.runType.data.end) ?? undefined; + const periodicScheduleIntervalTime = convertPeriodicTimeToSeconds(formData.runType.data.value); /* eslint-disable camelcase */ const data: CreatePipelineRunJobKFData = { name: formData.nameDesc.name, @@ -93,10 +94,7 @@ const createJob = async ( periodic_schedule: formData.runType.data.triggerType === ScheduledType.PERIODIC ? { - interval_second: - periodicOptionAsSeconds[ - formData.runType.data.value as keyof typeof periodicOptionAsSeconds - ].toString(), + interval_second: periodicScheduleIntervalTime.toString(), start_time: startDate, end_time: endDate, } diff --git a/frontend/src/concepts/pipelines/content/createRun/useRunFormData.ts b/frontend/src/concepts/pipelines/content/createRun/useRunFormData.ts index 337a5a731b..f971ecf8e5 100644 --- a/frontend/src/concepts/pipelines/content/createRun/useRunFormData.ts +++ b/frontend/src/concepts/pipelines/content/createRun/useRunFormData.ts @@ -2,8 +2,6 @@ import * as React from 'react'; import useGenericObjectState from '~/utilities/useGenericObjectState'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { - periodicOptionAsSeconds, - PeriodicOptions, RunDateTime, RunFormData, RunType, @@ -28,7 +26,7 @@ import { DEFAULT_PERIODIC_OPTION, DEFAULT_TIME, } from '~/concepts/pipelines/content/createRun/const'; -import { convertDateToTimeString } from '~/utilities/time'; +import { convertDateToTimeString, convertSecondsToPeriodicTime } from '~/utilities/time'; const isPipelineRunJob = ( runOrJob?: PipelineRunJobKF | PipelineRunKF, @@ -103,21 +101,6 @@ const parseKFTime = (kfTime?: DateTimeKF): RunDateTime | undefined => { return { date, time: time ?? DEFAULT_TIME }; }; -const intervalSecondsAsOption = (numberString?: string): PeriodicOptions => { - if (!numberString) { - return DEFAULT_PERIODIC_OPTION; - } - - const isPeriodicOption = (option: string): option is PeriodicOptions => option in PeriodicOptions; - const seconds = parseInt(numberString); - const option = Object.values(PeriodicOptions).find((o) => periodicOptionAsSeconds[o] === seconds); - if (!option || !isPeriodicOption(option)) { - return DEFAULT_PERIODIC_OPTION; - } - - return option; -}; - export const useUpdateRunType = ( setFunction: UpdateObjectAtPropAndValue, initialData?: PipelineRunKF | PipelineRunJobKF, @@ -139,7 +122,7 @@ export const useUpdateRunType = ( end = parseKFTime(trigger.cron_schedule.end_time); } else if (trigger.periodic_schedule) { triggerType = ScheduledType.PERIODIC; - value = intervalSecondsAsOption(trigger.periodic_schedule.interval_second); + value = convertSecondsToPeriodicTime(parseInt(trigger.periodic_schedule.interval_second)); start = parseKFTime(trigger.periodic_schedule.start_time); end = parseKFTime(trigger.periodic_schedule.end_time); } else { diff --git a/frontend/src/utilities/__tests__/string.spec.ts b/frontend/src/utilities/__tests__/string.spec.ts new file mode 100644 index 0000000000..814c3a9d2f --- /dev/null +++ b/frontend/src/utilities/__tests__/string.spec.ts @@ -0,0 +1,73 @@ +import { replaceNonNumericPartWithString, replaceNumericPartWithString } from '~/utilities/string'; + +describe('replaceNumericPartWithString', () => { + it('should replace the numeric part of a string with a number', () => { + expect(replaceNumericPartWithString('abc123xyz', 456)).toBe('abc456xyz'); + }); + + it('should handle empty input string', () => { + expect(replaceNumericPartWithString('', 789)).toBe('789'); + }); + + it('should handle input string without numeric part', () => { + expect(replaceNumericPartWithString('abcdef', 123)).toBe('123abcdef'); + }); + + it('should handle numeric part at the beginning of the string', () => { + expect(replaceNumericPartWithString('123xyz', 789)).toBe('789xyz'); + }); + + it('should handle numeric part at the end of the string', () => { + expect(replaceNumericPartWithString('abc456', 123)).toBe('abc123'); + }); + + it('should handle Pipeline scheduled time', () => { + expect(replaceNumericPartWithString('123Hour', 43424)).toBe('43424Hour'); + }); + + it('should handle default Pipeline scheduled time', () => { + expect(replaceNumericPartWithString('1Week', 26)).toBe('26Week'); + }); +}); + +describe('replaceNonNumericPartWithString', () => { + it('should replace the non-numeric part of a string with another string', () => { + expect(replaceNonNumericPartWithString('abc123xyz', 'XYZ')).toBe('XYZ123xyz'); + }); + + it('should handle empty input string', () => { + expect(replaceNonNumericPartWithString('', 'XYZ')).toBe('XYZ'); + }); + + it('should handle input string with no non-numeric part', () => { + expect(replaceNonNumericPartWithString('123', 'XYZ')).toBe('123XYZ'); + }); + + it('should handle input string with only non-numeric part', () => { + expect(replaceNonNumericPartWithString('abc', 'XYZ')).toBe('XYZ'); + }); + + it('should handle input string with multiple non-numeric parts', () => { + expect(replaceNonNumericPartWithString('abc123def456', 'XYZ')).toBe('XYZ123def456'); + }); + + it('should handle replacement string containing numbers', () => { + expect(replaceNonNumericPartWithString('abc123xyz', '123')).toBe('123123xyz'); + }); + + it('should handle replacement string containing special characters', () => { + expect(replaceNonNumericPartWithString('abc123xyz', '@#$')).toBe('@#$123xyz'); + }); + + it('should handle replacement string containing spaces', () => { + expect(replaceNonNumericPartWithString('abc123xyz', ' ')).toBe(' 123xyz'); + }); + + it('should handle Pipeline scheduled time', () => { + expect(replaceNonNumericPartWithString('123Week', 'Minute')).toBe('123Minute'); + }); + + it('should handle default Pipeline scheduled time', () => { + expect(replaceNonNumericPartWithString('1Week', 'Minute')).toBe('1Minute'); + }); +}); diff --git a/frontend/src/utilities/__tests__/time.spec.ts b/frontend/src/utilities/__tests__/time.spec.ts new file mode 100644 index 0000000000..a2094a43c5 --- /dev/null +++ b/frontend/src/utilities/__tests__/time.spec.ts @@ -0,0 +1,41 @@ +import { convertPeriodicTimeToSeconds, convertSecondsToPeriodicTime } from '~/utilities/time'; + +describe('Convert periodic time to seconds', () => { + it('should convert hours to seconds', () => { + expect(convertPeriodicTimeToSeconds('5Hour')).toBe(5 * 60 * 60); + }); + + it('should convert minutes to seconds', () => { + expect(convertPeriodicTimeToSeconds('6Minute')).toBe(6 * 60); + }); + + it('should convert days to seconds', () => { + expect(convertPeriodicTimeToSeconds('221Day')).toBe(221 * 24 * 60 * 60); + }); + + it('should convert weeks to seconds', () => { + expect(convertPeriodicTimeToSeconds('12Week')).toBe(12 * 7 * 24 * 60 * 60); + }); + + it('should default to 0 seconds for unrecognized units', () => { + expect(convertPeriodicTimeToSeconds('3Weeks')).toBe(0); + }); +}); + +describe('Convert seconds to periodic time', () => { + it('should convert seconds to minutes', () => { + expect(convertSecondsToPeriodicTime(120)).toBe('2Minute'); + }); + + it('should convert seconds to hours', () => { + expect(convertSecondsToPeriodicTime(7200)).toBe('2Hour'); + }); + + it('should convert seconds to days', () => { + expect(convertSecondsToPeriodicTime(172800)).toBe('2Day'); + }); + + it('should convert seconds to weeks', () => { + expect(convertSecondsToPeriodicTime(604800)).toBe('1Week'); + }); +}); diff --git a/frontend/src/utilities/string.ts b/frontend/src/utilities/string.ts index f0299b1a4a..4d472fbe7b 100644 --- a/frontend/src/utilities/string.ts +++ b/frontend/src/utilities/string.ts @@ -3,3 +3,58 @@ export const genRandomChars = (len = 6): string => .toString(36) .replace(/[^a-z0-9]+/g, '') .substr(1, len); + +/** + * This function replaces the first occurrence of a numeric part in the input string + * with the specified replacement numeric value. + * @param inputString + * @param replacementString + */ +export const replaceNumericPartWithString = ( + inputString: string, + replacementString: number, +): string => { + // If the input string is empty or contains only whitespace, return the replacement as a string. + if (inputString.trim() === '') { + return replacementString.toString(); + } + + const match = inputString.match(/\d+/); //Find numeric part in string (only first occurance) + let updatedString = inputString; + + if (match) { + const matchedNumber = match[0]; + updatedString = inputString.replace(matchedNumber, String(replacementString)); + } else { + // If no numeric part is found, prepend the replacement numeric value to the input string. + updatedString = replacementString + inputString; + } + return updatedString; +}; + +/** + * This function replaces the first occurrence of a non-numeric part in the input string + * with the specified replacement string. + * @param inputString + * @param replacementString + */ +export const replaceNonNumericPartWithString = ( + inputString: string, + replacementString: string, +): string => { + if (inputString.trim() === '') { + return replacementString; + } + + const match = inputString.match(/\D+/); //Find non-numeric part in string (only first occurance) + let updatedString = inputString; + + if (match) { + const matchedString = match[0]; + updatedString = inputString.replace(matchedString, replacementString); + } else { + // If no non-numeric part is found, append the replacement non-numeric value to the input string. + updatedString = inputString + replacementString; + } + return updatedString; +}; diff --git a/frontend/src/utilities/time.ts b/frontend/src/utilities/time.ts index 5764182522..a245017a24 100644 --- a/frontend/src/utilities/time.ts +++ b/frontend/src/utilities/time.ts @@ -1,3 +1,8 @@ +import { + PeriodicOptions, + periodicOptionAsSeconds, +} from '~/concepts/pipelines/content/createRun/types'; + const printAgo = (time: number, unit: string) => `${time} ${unit}${time > 1 ? 's' : ''} ago`; const printIn = (time: number, unit: string) => `in ${time} ${unit}${time > 1 ? 's' : ''}`; const leadZero = (v: number) => (v < 10 ? `0${v}` : `${v}`); @@ -154,3 +159,45 @@ export const relativeTime = (current: number, previous: number): string => { return `${date.getDate()} ${monthAsString} ${date.getFullYear()}`; }; + +/** Function to convert time strings like "2Hour" to seconds */ +export const convertPeriodicTimeToSeconds = (timeString: string): number => { + let numericValue = parseInt(timeString, 10); + + if (isNaN(numericValue)) { + numericValue = 1; + } + + const unit = timeString.toLowerCase().replace(/\d+/g, ''); + + switch (unit) { + case 'hour': + return numericValue * 60 * 60; + case 'minute': + return numericValue * 60; + case 'day': + return numericValue * 24 * 60 * 60; + case 'week': + return numericValue * 7 * 24 * 60 * 60; + default: + return 0; + } +}; + +/** Function to convert seconds to time strings like "2Hour" */ +export const convertSecondsToPeriodicTime = (seconds: number): string => { + const units = Object.values(PeriodicOptions).reverse(); + const unitFactors = Object.values(periodicOptionAsSeconds).reverse(); + + for (let i = 0; i < units.length; i++) { + const unit = units[i]; + const unitFactor = unitFactors[i]; + + if (seconds >= unitFactor) { + const count = Math.floor(seconds / unitFactor); + return `${count}${unit}`; + } + } + + return ''; +};