Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a numerical input to Create Periodic Scheduled Runs #1768

Merged
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
2 changes: 1 addition & 1 deletion frontend/src/concepts/pipelines/content/createRun/const.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -32,22 +42,47 @@ const RunTypeSectionScheduled: React.FC<RunTypeSectionScheduledProps> = ({ 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 && (
<>
<Text>
<b>Run every</b>
</Text>
<SimpleDropdownSelect
options={Object.values(PeriodicOptions).map((v) => ({
key: v,
label: v,
}))}
value={data.value}
onChange={(value) => onChange({ ...data, value })}
/>
<Split hasGutter>
<SplitItem>
<NumberInputWrapper
min={1}
value={parseInt(data.value) || 1}
onChange={(value) =>
onChange({
...data,
value: replaceNumericPartWithString(data.value, value),
})
}
/>
</SplitItem>
<SplitItem>
<SimpleDropdownSelect
options={Object.values(PeriodicOptions).map((v) => ({
key: v,
label: v,
}))}
value={data.value.replace(/\d+/, '')}
onChange={(value) =>
onChange({
...data,
value: replaceNonNumericPartWithString(data.value, value),
})
}
/>
</SplitItem>
</Split>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
periodicOptionAsSeconds,
RunDateTime,
RunFormData,
RunTypeOption,
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<RunFormData>,
initialData?: PipelineRunKF | PipelineRunJobKF,
Expand All @@ -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 {
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/utilities/__tests__/string.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
41 changes: 41 additions & 0 deletions frontend/src/utilities/__tests__/time.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
55 changes: 55 additions & 0 deletions frontend/src/utilities/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Loading
Loading