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

pull/11905/head
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

@ -122,7 +122,7 @@ angular
url: '/new',
views: {
'content@': {
component: 'createEdgeJobView',
component: 'edgeJobsCreateView',
},
},
};

View File

@ -36,7 +36,7 @@
<!-- edge-job-method-select -->
<div class="col-sm-12 form-section-title"> Edge job configuration </div>
<box-selector slim="true" radio-name="'configuration'" value="$ctrl.formValues.cronMethod" options="$ctrl.cronMethods" on-change="($ctrl.onCronMethodChange)"></box-selector>
<box-selector slim="true" radio-name="'configurationold'" value="$ctrl.formValues.cronMethod" options="$ctrl.cronMethods" on-change="($ctrl.onCronMethodChange)"></box-selector>
<!-- !edge-job-method-select -->
<!-- basic-edge-job -->
@ -44,9 +44,10 @@
<div class="form-group">
<label for="recurring" class="col-sm-2 control-label text-left">Recurring Edge job</label>
<div class="col-sm-10">
<label class="switch"
><input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring" data-cy="recurring-edge-job-checkbox" /><span class="slider round"></span
></label>
<label class="switch">
<input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring" id="recurring" data-cy="recurring-edge-job-checkbox" />
<span class="slider round"></span>
</label>
</div>
</div>
<!-- not-recurring -->
@ -135,7 +136,7 @@
<!-- execution-method -->
<div ng-if="!$ctrl.model.Id">
<div class="col-sm-12 form-section-title"> Job content </div>
<box-selector value="$ctrl.formValues.method" options="$ctrl.buildMethods" radio-name="buildMethod" on-change="($ctrl.onBuildMethodChange)" slim="true"></box-selector>
<box-selector value="$ctrl.formValues.method" options="$ctrl.buildMethods" radio-name="'buildMethodolds'" on-change="($ctrl.onBuildMethodChange)" slim="true"></box-selector>
</div>
<!-- !execution-method -->

View File

@ -9,8 +9,8 @@ export class EdgeJobFormController {
this.$scope = $scope;
this.$async = $async;
this.cronMethods = cronMethodOptions;
this.buildMethods = [editor, upload];
this.cronMethods = cronMethodOptions.map((o) => ({ ...o, id: o.id + '-old' }));
this.buildMethods = [editor, upload].map((o) => ({ ...o, id: o.id + '-old' }));
this.state = {
formValidationError: '',
@ -70,10 +70,12 @@ export class EdgeJobFormController {
onChangeModel(model) {
const defaultTime = moment().add('hours', 1);
const scheduled = this.scheduleValues.find((v) => v.cron === model.CronExpression);
this.formValues = {
datetime: model.CronExpression ? cronToDatetime(model.CronExpression, defaultTime) : defaultTime,
scheduleValue: this.formValues.scheduleValue,
cronMethod: model.Recurring ? 'advanced' : 'basic',
scheduleValue: scheduled || this.scheduleValues[0],
cronMethod: model.Recurring && !scheduled ? 'advanced' : 'basic',
method: this.formValues.method,
};
}

View File

@ -4,10 +4,12 @@ import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { ListView } from '@/react/edge/edge-jobs/ListView';
import { CreateView } from '@/react/edge/edge-jobs/CreateView/CreateView';
export const jobsModule = angular
.module('portainer.edge.react.views.jobs', [])
.component('edgeJobsView', r2a(withUIRouter(withCurrentUser(ListView)), []))
.component(
'edgeJobsView',
r2a(withUIRouter(withCurrentUser(ListView)), [])
'edgeJobsCreateView',
r2a(withUIRouter(withCurrentUser(CreateView)), [])
).name;

View File

@ -1,20 +0,0 @@
<page-header title="'Create Edge job'" breadcrumbs="[{label:'Edge Jobs', link:'edge.jobs'}, 'Create Edge job']"> </page-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<edge-job-form
model="$ctrl.model"
groups="$ctrl.groups"
tags="$ctrl.tags"
edge-groups="$ctrl.edgeGroups"
form-action="$ctrl.create"
form-action-label="Create edge job"
action-in-progress="$ctrl.state.actionInProgress"
is-editor-dirty="$ctrl.state.isEditorDirty"
></edge-job-form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -1,86 +0,0 @@
import { confirmWebEditorDiscard } from '@@/modals/confirm';
export class CreateEdgeJobViewController {
/* @ngInject */
constructor($async, $q, $state, $window, EdgeJobService, GroupService, Notifications, TagService) {
this.state = {
actionInProgress: false,
isEditorDirty: false,
};
this.model = {
Name: '',
Recurring: false,
CronExpression: '',
Endpoints: [],
FileContent: '',
File: null,
EdgeGroups: [],
};
this.$async = $async;
this.$q = $q;
this.$state = $state;
this.$window = $window;
this.Notifications = Notifications;
this.GroupService = GroupService;
this.EdgeJobService = EdgeJobService;
this.TagService = TagService;
this.create = this.create.bind(this);
this.createEdgeJob = this.createEdgeJob.bind(this);
this.createAsync = this.createAsync.bind(this);
}
create(method) {
return this.$async(this.createAsync, method);
}
async createAsync(method) {
this.state.actionInProgress = true;
try {
await this.createEdgeJob(method, this.model);
this.Notifications.success('Success', 'Edge job successfully created');
this.state.isEditorDirty = false;
this.$state.go('edge.jobs', {}, { reload: true });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create Edge job');
}
this.state.actionInProgress = false;
}
createEdgeJob(method, model) {
if (method === 'editor') {
return this.EdgeJobService.createEdgeJobFromFileContent(model);
}
return this.EdgeJobService.createEdgeJobFromFileUpload(model);
}
async uiCanExit() {
if (this.model.FileContent && this.state.isEditorDirty) {
return confirmWebEditorDiscard();
}
}
async $onInit() {
try {
const [groups, tags] = await Promise.all([this.GroupService.groups(), this.TagService.tags()]);
this.groups = groups;
this.tags = tags;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve page data');
}
this.$window.onbeforeunload = () => {
if (this.model.FileContent && this.state.isEditorDirty) {
return '';
}
};
}
$onDestroy() {
this.state.isEditorDirty = false;
}
}

View File

@ -1,7 +0,0 @@
import angular from 'angular';
import { CreateEdgeJobViewController } from './createEdgeJobViewController';
angular.module('portainer.edge').component('createEdgeJobView', {
templateUrl: './createEdgeJobView.html',
controller: CreateEdgeJobViewController,
});

View File

@ -0,0 +1,57 @@
import DateTimePicker from 'react-datetime-picker';
import { Calendar, X } from 'lucide-react';
import { isoDate } from '@/portainer/filters/filters';
import { AutomationTestingProps } from '@/types';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import 'react-datetime-picker/dist/DateTimePicker.css';
import 'react-calendar/dist/Calendar.css';
export const FORMAT = 'YYYY-MM-DD HH:mm';
export function DateTimeField({
error,
label,
disabled,
name,
value,
onChange,
minDate,
'data-cy': dataCy,
}: {
error?: string;
disabled?: boolean;
name: string;
value: Date | null;
onChange: (date: Date | null) => void;
label: string;
minDate?: Date;
} & AutomationTestingProps) {
return (
<FormControl label={label} errors={error}>
{!disabled ? (
<DateTimePicker
format="y-MM-dd HH:mm"
className="form-control [&>div]:border-0"
name={name}
value={value}
onChange={onChange}
calendarIcon={<Calendar className="lucide" />}
clearIcon={<X className="lucide" />}
disableClock
data-cy={dataCy}
minDate={minDate}
/>
) : (
<Input
defaultValue={isoDate(value?.valueOf(), FORMAT)}
disabled
data-cy={`${dataCy}-disabled-value`}
/>
)}
</FormControl>
);
}

View File

@ -59,6 +59,7 @@ interface Props extends AutomationTestingProps {
id: string;
placeholder?: string;
yaml?: boolean;
shell?: boolean;
readonly?: boolean;
titleContent?: React.ReactNode;
hideTitle?: boolean;
@ -77,6 +78,7 @@ export function WebEditorForm({
hideTitle,
readonly,
yaml,
shell,
children,
error,
versions,
@ -108,6 +110,7 @@ export function WebEditorForm({
placeholder={placeholder}
readonly={readonly}
yaml={yaml}
shell={shell}
value={value}
onChange={onChange}
versions={versions}

View File

@ -11,7 +11,7 @@ export interface Option<T extends string | number>
}
interface Props<T extends string | number> extends AutomationTestingProps {
options: Option<T>[];
options: Array<Option<T>> | ReadonlyArray<Option<T>>;
}
export function Select<T extends number | string>({

View File

@ -0,0 +1,203 @@
import { Form, Formik, useFormikContext } from 'formik';
import { useRouter } from '@uirouter/react';
import moment from 'moment';
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
import { notifySuccess } from '@/portainer/services/notifications';
import { FormActions } from '@@/form-components/FormActions';
import { FormSection } from '@@/form-components/FormSection';
import { BoxSelector } from '@@/BoxSelector';
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
import { WebEditorForm } from '@@/WebEditorForm';
import { FileUploadForm } from '@@/form-components/FileUpload';
import { NameField } from '../components/EdgeJobForm/NameField';
import { FormValues } from '../components/EdgeJobForm/types';
import { useValidation } from '../components/EdgeJobForm/useValidation';
import { JobConfigurationFieldset } from '../components/EdgeJobForm/JobConfigurationFieldset';
import {
BasePayload,
CreateEdgeJobPayload,
useCreateEdgeJobMutation,
} from '../queries/useCreateEdgeJobMutation/useCreateEdgeJobMutation';
import { defaultCronExpression } from '../components/EdgeJobForm/RecurringFieldset';
export function CreateEdgeJobForm() {
const mutation = useCreateEdgeJobMutation();
const validation = useValidation();
const router = useRouter();
return (
<Formik<FormValues>
validationSchema={validation}
validateOnMount
initialValues={{
name: '',
recurring: false,
cronExpression: '',
recurringOption: defaultCronExpression,
method: 'editor',
cronMethod: 'basic',
dateTime: new Date(),
edgeGroupIds: [],
environmentIds: [],
file: undefined,
fileContent: '',
}}
onSubmit={(values) => {
mutation.mutate(getPayload(values.method, values), {
onSuccess: () => {
notifySuccess('Success', 'Edge job successfully created');
router.stateService.go('^');
},
});
}}
>
<InnerForm isLoading={mutation.isLoading} />
</Formik>
);
}
const buildMethods = [editor, upload];
function InnerForm({ isLoading }: { isLoading: boolean }) {
const { values, setFieldValue, isValid, errors } =
useFormikContext<FormValues>();
return (
<Form className="form-horizontal">
<NameField errors={errors.name} />
<JobConfigurationFieldset />
<FormSection title="Job content">
<BoxSelector
value={values.method}
options={buildMethods}
onChange={(value) => setFieldValue('method', value)}
radioName="build-method"
/>
</FormSection>
{values.method === 'editor' && (
<WebEditorForm
data-cy="edge-job-editor"
id="edge-job-editor"
onChange={(value) => setFieldValue('fileContent', value)}
value={values.fileContent}
placeholder="Define or paste the content of your script file here"
shell
error={errors.fileContent}
/>
)}
{values.method === 'upload' && (
<FileUploadForm
data-cy="edge-job-upload"
description="You can upload a script file from your computer."
onChange={(value) => setFieldValue('file', value)}
value={values.file}
required
/>
)}
<EdgeGroupsSelector
onChange={(value) => setFieldValue('edgeGroupIds', value)}
value={values.edgeGroupIds}
error={errors.edgeGroupIds}
/>
<FormSection title="Target environments">
<AssociatedEdgeEnvironmentsSelector
onChange={(value) => setFieldValue('environmentIds', value)}
value={values.environmentIds}
/>
</FormSection>
<FormActions
submitLabel="Add edge job"
isLoading={isLoading}
isValid={isValid}
data-cy="edgeJobCreate-addJobButton"
loadingText="In progress..."
/>
</Form>
);
}
function getPayload(
method: 'upload' | 'editor',
values: FormValues
): CreateEdgeJobPayload {
switch (method) {
case 'upload':
if (!values.file) {
throw new Error('File is required');
}
return {
method: 'file',
payload: {
...getBasePayload(values),
file: values.file,
},
};
case 'editor':
return {
method: 'string',
payload: {
...getBasePayload(values),
fileContent: values.fileContent,
},
};
default:
throw new Error(`Unknown method: ${method}`);
}
function getBasePayload(values: FormValues): BasePayload {
return {
name: values.name,
edgeGroups: values.edgeGroupIds,
endpoints: values.environmentIds,
...getRecurringConfig(values),
};
}
function getRecurringConfig(values: FormValues): {
recurring: boolean;
cronExpression: string;
} {
if (values.cronMethod !== 'basic') {
return {
recurring: true,
cronExpression: values.cronExpression,
};
}
if (values.recurring) {
return {
recurring: true,
cronExpression: values.recurringOption,
};
}
return {
recurring: false,
cronExpression: dateTimeToCron(values.dateTime),
};
function dateTimeToCron(datetime: Date) {
const date = moment(datetime);
return [
date.minutes(),
date.hours(),
date.date(),
date.month() + 1,
'*',
].join(' ');
}
}
}

View File

@ -0,0 +1,28 @@
import { PageHeader } from '@@/PageHeader';
import { Widget } from '@@/Widget';
import { CreateEdgeJobForm } from './CreateEdgeJobForm';
export function CreateView() {
return (
<>
<PageHeader
title="Create edge job"
breadcrumbs={[
{ label: 'Edge jobs', link: 'edge.jobs' },
'Create edge job',
]}
/>
<div className="row">
<div className="col-sm-12">
<Widget>
<Widget.Body>
<CreateEdgeJobForm />
</Widget.Body>
</Widget>
</div>
</div>
</>
);
}

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]
);
}

View File

@ -0,0 +1,39 @@
import axios, {
json2formData,
parseAxiosError,
} from '@/portainer/services/axios';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { EdgeJob } from '../../types';
import { buildUrl } from '../build-url';
/**
* Payload to create an EdgeJob from a file
*/
export type FileUploadPayload = {
Name: string;
CronExpression: string;
Recurring: boolean;
EdgeGroups: Array<EdgeGroup['Id']>;
Endpoints: Array<EnvironmentId>;
File: File;
};
export async function createJobFromFile(payload: FileUploadPayload) {
try {
const { data } = await axios.post<EdgeJob>(
buildUrl({ action: 'create/file' }),
json2formData(payload),
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View File

@ -0,0 +1,31 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { EdgeJob } from '../../types';
import { buildUrl } from '../build-url';
/**
* Payload for creating an EdgeJob from a string
*/
export interface FileContentPayload {
name: string;
cronExpression: string;
recurring: boolean;
edgeGroups: Array<EdgeGroup['Id']>;
endpoints: Array<EnvironmentId>;
fileContent: string;
}
export async function createJobFromFileContent(payload: FileContentPayload) {
try {
const { data } = await axios.post<EdgeJob>(
buildUrl({ action: 'create/string' }),
payload
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View File

@ -0,0 +1,62 @@
import { useMutation } from '@tanstack/react-query';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { createJobFromFile } from './createJobFromFile';
import { createJobFromFileContent } from './createJobFromFileContent';
export function useCreateEdgeJobMutation() {
return useMutation(createEdgeJob);
}
export type BasePayload = {
name: string;
cronExpression: string;
recurring: boolean;
edgeGroups: Array<EdgeGroup['Id']>;
endpoints: Array<EnvironmentId>;
};
export type CreateEdgeJobPayload =
| {
method: 'file';
payload: BasePayload & {
/** File to upload */
file: File;
};
}
| {
method: 'string';
payload: BasePayload & {
/** Content of the Job file */
fileContent: string;
};
};
function createEdgeJob({ method, payload }: CreateEdgeJobPayload) {
switch (method) {
case 'file':
return createJobFromFile({
CronExpression: payload.cronExpression,
Recurring: payload.recurring,
Name: payload.name,
EdgeGroups: payload.edgeGroups,
Endpoints: payload.endpoints,
File: payload.file,
});
case 'string':
return createJobFromFileContent({
cronExpression: payload.cronExpression,
recurring: payload.recurring,
name: payload.name,
edgeGroups: payload.edgeGroups,
endpoints: payload.endpoints,
fileContent: payload.fileContent,
});
default:
throw new Error('Invalid method');
}
}

View File

@ -1,6 +1,6 @@
import { formatDate } from '@/portainer/filters/filters';
import { FORMAT } from '../../common/ScheduledTimeField';
import { FORMAT } from '@@/DateTimeField';
import { columnHelper } from './helper';

View File

@ -1,8 +1,6 @@
import DateTimePicker from 'react-datetime-picker';
import { Calendar, X } from 'lucide-react';
import { useField } from 'formik';
import { useMemo } from 'react';
import { string } from 'yup';
import { useField } from 'formik';
import {
isoDate,
@ -10,25 +8,18 @@ import {
TIME_FORMAT,
} from '@/portainer/filters/filters';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { DateTimeField, FORMAT } from '@@/DateTimeField';
import { TextTip } from '@@/Tip/TextTip';
import { FormValues } from './types';
import 'react-datetime-picker/dist/DateTimePicker.css';
import 'react-calendar/dist/Calendar.css';
interface Props {
disabled?: boolean;
}
export const FORMAT = 'YYYY-MM-DD HH:mm';
export function ScheduledTimeField({ disabled }: Props) {
const [{ name, value }, { error }, { setValue }] =
useField<FormValues['scheduledTime']>('scheduledTime');
const dateValue = useMemo(() => parseIsoDate(value, FORMAT), [value]);
if (!value) {
@ -37,31 +28,19 @@ export function ScheduledTimeField({ disabled }: Props) {
return (
<>
<FormControl label="Schedule date & time" errors={error}>
{!disabled ? (
<DateTimePicker
format="y-MM-dd HH:mm"
className="form-control [&>div]:border-0"
onChange={(date) => {
const dateToSave =
date || new Date(Date.now() + 24 * 60 * 60 * 1000);
setValue(isoDate(dateToSave.valueOf(), FORMAT));
}}
name={name}
value={dateValue}
calendarIcon={<Calendar className="lucide" />}
clearIcon={<X className="lucide" />}
disableClock
minDate={new Date(Date.now() - 24 * 60 * 60 * 1000)}
/>
) : (
<Input
defaultValue={value}
disabled
data-cy="update-schedules-time-input"
/>
)}
</FormControl>
<DateTimeField
label="Schedule date & time"
minDate={new Date(Date.now() - 24 * 60 * 60 * 1000)}
onChange={(date) => {
const dateToSave = date || new Date(Date.now() + 24 * 60 * 60 * 1000);
setValue(isoDate(dateToSave.valueOf(), FORMAT));
}}
error={error}
disabled={disabled}
name={name}
value={dateValue}
data-cy="update-schedules-time-input"
/>
{!disabled && value && (
<TextTip color="blue">
If time zone is not set on edge agent then UTC+0 will be used.