1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 22:05:23 +02:00

feat(edge/jobs): migrate create view to react [EE-2221] (#11867)

This commit is contained in:
Chaim Lev-Ari 2024-06-02 11:10:38 +03:00 committed by GitHub
parent 94c91035a7
commit 02fbdfec36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 777 additions and 163 deletions

View file

@ -0,0 +1,40 @@
import { useField } from 'formik';
import { string } from 'yup';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { TimeTip } from './TimeTip';
export function AdvancedCronFieldset() {
const [{ value, onChange, name, onBlur }, { error }] =
useField<string>('cronExpression');
return (
<>
<FormControl label="Cron rule" inputId="edge_job_cron" errors={error}>
<Input
data-cy="edge-job-cron-input"
id="edge_job_cron"
placeholder="e.g. 0 2 * * *"
required
value={value}
onChange={onChange}
name={name}
onBlur={onBlur}
/>
</FormControl>
<TimeTip />
</>
);
}
/** https://regexr.com/573i2 */
const cronRegex =
/(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ){4,6}((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*))/;
export function cronValidation() {
return string()
.default('')
.matches(cronRegex, 'This field format is invalid.');
}

View file

@ -0,0 +1,31 @@
import { useFormikContext } from 'formik';
import { SwitchField } from '@@/form-components/SwitchField';
import { FormValues } from './types';
import { RecurringFieldset, defaultCronExpression } from './RecurringFieldset';
import { ScheduledDateFieldset } from './ScheduledDateFieldset';
export function BasicCronFieldset() {
const { values, setFieldValue } = useFormikContext<FormValues>();
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Recurring Edge job"
checked={values.recurring}
onChange={(value) => {
setFieldValue('recurring', value);
if (value) {
setFieldValue('recurringOption', defaultCronExpression);
}
}}
data-cy="edgeJobCreate-recurringSwitch"
/>
</div>
</div>
{values.recurring ? <RecurringFieldset /> : <ScheduledDateFieldset />}
</>
);
}

View file

@ -0,0 +1,37 @@
import { useFormikContext } from 'formik';
import { FormSection } from '@@/form-components/FormSection';
import { BoxSelector } from '@@/BoxSelector';
import { cronMethodOptions } from '../../CreateView/cron-method-options';
import { FormValues } from './types';
import { AdvancedCronFieldset } from './AdvancedCronFieldset';
import { BasicCronFieldset } from './BasicCronFieldset';
export function JobConfigurationFieldset() {
const { values, setFieldValue } = useFormikContext<FormValues>();
return (
<>
<FormSection title="Edge job configuration">
<BoxSelector
slim
radioName="configuration"
value={values.cronMethod}
options={cronMethodOptions}
onChange={(value) => {
setFieldValue('cronMethod', value);
setFieldValue('cronExpression', '');
}}
/>
</FormSection>
{values.cronMethod === 'basic' ? (
<BasicCronFieldset />
) : (
<AdvancedCronFieldset />
)}
</>
);
}

View file

@ -0,0 +1,46 @@
import { Field, FormikErrors } from 'formik';
import { string } from 'yup';
import { useMemo } from 'react';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { useEdgeJobs } from '../../queries/useEdgeJobs';
import { EdgeJob } from '../../types';
export function NameField({ errors }: { errors?: FormikErrors<string> }) {
return (
<FormControl label="Name" required errors={errors} inputId="edgejob_name">
<Field
as={Input}
name="name"
placeholder="e.g. backup-app-prod"
data-cy="edgejob-name-input"
id="edgejob_name"
/>
</FormControl>
);
}
export function useNameValidation(id?: EdgeJob['Id']) {
const edgeJobsQuery = useEdgeJobs();
return useMemo(
() =>
string()
.required('Name is required')
.matches(
/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/,
'Allowed characters are: [a-zA-Z0-9_.-]'
)
.test({
name: 'is-unique',
test: (value) =>
!edgeJobsQuery.data?.find(
(job) => job.Name === value && job.Id !== id
),
message: 'Name must be unique',
}),
[edgeJobsQuery.data, id]
);
}

View file

@ -0,0 +1,40 @@
import { useField } from 'formik';
import { FormControl } from '@@/form-components/FormControl';
import { Select } from '@@/form-components/Input';
export const defaultCronExpression = '0 * * * *' as const;
export const timeOptions = [
{
label: 'Every hour',
value: defaultCronExpression,
},
{
label: 'Every 2 hours',
value: '0 */2 * * *',
},
{
label: 'Every day',
value: '0 0 * * *',
},
] as const;
export function RecurringFieldset() {
const [{ value, onChange, name, onBlur }, { error }] =
useField<string>('recurringOption');
return (
<FormControl label="Edge job time" inputId="edge_job_value" errors={error}>
<Select
id="edge_job_value"
data-cy="edge-job-time-select"
name={name}
options={timeOptions}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
</FormControl>
);
}

View file

@ -0,0 +1,25 @@
import { useField } from 'formik';
import { DateTimeField } from '@@/DateTimeField';
import { TimeTip } from './TimeTip';
export function ScheduledDateFieldset() {
const [{ value }, { error }, { setValue }] = useField<Date | null>(
'dateTime'
);
return (
<>
<DateTimeField
value={value}
onChange={(date) => setValue(date)}
error={error}
label="Scheduled date"
name="dateTime"
data-cy="edge-job-date-time-picker"
/>
<TimeTip />
</>
);
}

View file

@ -0,0 +1,9 @@
import { TextTip } from '@@/Tip/TextTip';
export function TimeTip() {
return (
<TextTip color="blue">
Time should be set according to the chosen environments&apos; timezone.
</TextTip>
);
}

View file

@ -0,0 +1,20 @@
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { timeOptions } from './RecurringFieldset';
export interface FormValues {
name: string;
recurring: boolean;
edgeGroupIds: Array<EdgeGroup['Id']>;
environmentIds: Array<EnvironmentId>;
method: 'editor' | 'upload';
fileContent: string;
file: File | undefined;
cronMethod: 'basic' | 'advanced';
dateTime: Date; // basic !recurring
recurringOption: (typeof timeOptions)[number]['value']; // basic recurring
cronExpression: string; // advanced
}

View file

@ -0,0 +1,72 @@
import {
SchemaOf,
array,
boolean,
date,
mixed,
number,
object,
string,
} from 'yup';
import { useMemo } from 'react';
import { file } from '@@/form-components/yup-file-validation';
import { EdgeJob } from '../../types';
import { FormValues } from './types';
import { useNameValidation } from './NameField';
import { cronValidation } from './AdvancedCronFieldset';
import { timeOptions } from './RecurringFieldset';
export function useValidation({
id,
}: { id?: EdgeJob['Id'] } = {}): SchemaOf<FormValues> {
const nameValidation = useNameValidation(id);
return useMemo(
() =>
object({
name: nameValidation,
recurring: boolean().default(false),
cronExpression: string().default('').when('cronMethod', {
is: 'advanced',
then: cronValidation().required(),
}),
edgeGroupIds: array(number().required()),
environmentIds: array(number().required()),
method: mixed<'editor' | 'upload'>()
.oneOf(['editor', 'upload'])
.default('editor'),
file: file().when('method', {
is: 'upload',
then: object().required('This field is required.'),
}),
fileContent: string()
.default('')
.when('method', {
is: 'editor',
then: (schema) => schema.required('This field is required.'),
}),
cronMethod: mixed<'basic' | 'advanced'>()
.oneOf(['basic', 'advanced'])
.default('basic'),
dateTime: date()
.default(new Date())
.when(['recurring', 'cronMethod'], {
is: (recurring: boolean, cronMethod: 'basic' | 'advanced') =>
!recurring && cronMethod === 'basic',
then: (schema) => schema.required('This field is required.'),
}),
recurringOption: mixed()
.oneOf(timeOptions.map((o) => o.value))
.when(['recurring', 'cronMethod'], {
is: (recurring: boolean, cronMethod: 'basic' | 'advanced') =>
recurring && cronMethod === 'basic',
then: (schema) => schema.required('This field is required.'),
}),
}),
[nameValidation]
);
}