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:
parent
94c91035a7
commit
02fbdfec36
27 changed files with 777 additions and 163 deletions
|
@ -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.');
|
||||
}
|
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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]
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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' timezone.
|
||||
</TextTip>
|
||||
);
|
||||
}
|
20
app/react/edge/edge-jobs/components/EdgeJobForm/types.ts
Normal file
20
app/react/edge/edge-jobs/components/EdgeJobForm/types.ts
Normal 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
|
||||
}
|
|
@ -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]
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue