mirror of https://github.com/portainer/portainer
feat(edge/jobs): migrate create view to react [EE-2221] (#11867)
parent
94c91035a7
commit
02fbdfec36
|
@ -122,7 +122,7 @@ angular
|
||||||
url: '/new',
|
url: '/new',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'createEdgeJobView',
|
component: 'edgeJobsCreateView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<!-- edge-job-method-select -->
|
<!-- edge-job-method-select -->
|
||||||
<div class="col-sm-12 form-section-title"> Edge job configuration </div>
|
<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 -->
|
<!-- !edge-job-method-select -->
|
||||||
<!-- basic-edge-job -->
|
<!-- basic-edge-job -->
|
||||||
|
@ -44,9 +44,10 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="recurring" class="col-sm-2 control-label text-left">Recurring Edge job</label>
|
<label for="recurring" class="col-sm-2 control-label text-left">Recurring Edge job</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<label class="switch"
|
<label class="switch">
|
||||||
><input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring" data-cy="recurring-edge-job-checkbox" /><span class="slider round"></span
|
<input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring" id="recurring" data-cy="recurring-edge-job-checkbox" />
|
||||||
></label>
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- not-recurring -->
|
<!-- not-recurring -->
|
||||||
|
@ -135,7 +136,7 @@
|
||||||
<!-- execution-method -->
|
<!-- execution-method -->
|
||||||
<div ng-if="!$ctrl.model.Id">
|
<div ng-if="!$ctrl.model.Id">
|
||||||
<div class="col-sm-12 form-section-title"> Job content </div>
|
<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>
|
</div>
|
||||||
<!-- !execution-method -->
|
<!-- !execution-method -->
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ export class EdgeJobFormController {
|
||||||
this.$scope = $scope;
|
this.$scope = $scope;
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
|
|
||||||
this.cronMethods = cronMethodOptions;
|
this.cronMethods = cronMethodOptions.map((o) => ({ ...o, id: o.id + '-old' }));
|
||||||
this.buildMethods = [editor, upload];
|
this.buildMethods = [editor, upload].map((o) => ({ ...o, id: o.id + '-old' }));
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
formValidationError: '',
|
formValidationError: '',
|
||||||
|
@ -70,10 +70,12 @@ export class EdgeJobFormController {
|
||||||
|
|
||||||
onChangeModel(model) {
|
onChangeModel(model) {
|
||||||
const defaultTime = moment().add('hours', 1);
|
const defaultTime = moment().add('hours', 1);
|
||||||
|
const scheduled = this.scheduleValues.find((v) => v.cron === model.CronExpression);
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
datetime: model.CronExpression ? cronToDatetime(model.CronExpression, defaultTime) : defaultTime,
|
datetime: model.CronExpression ? cronToDatetime(model.CronExpression, defaultTime) : defaultTime,
|
||||||
scheduleValue: this.formValues.scheduleValue,
|
scheduleValue: scheduled || this.scheduleValues[0],
|
||||||
cronMethod: model.Recurring ? 'advanced' : 'basic',
|
cronMethod: model.Recurring && !scheduled ? 'advanced' : 'basic',
|
||||||
method: this.formValues.method,
|
method: this.formValues.method,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { r2a } from '@/react-tools/react2angular';
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { ListView } from '@/react/edge/edge-jobs/ListView';
|
import { ListView } from '@/react/edge/edge-jobs/ListView';
|
||||||
|
import { CreateView } from '@/react/edge/edge-jobs/CreateView/CreateView';
|
||||||
|
|
||||||
export const jobsModule = angular
|
export const jobsModule = angular
|
||||||
.module('portainer.edge.react.views.jobs', [])
|
.module('portainer.edge.react.views.jobs', [])
|
||||||
|
.component('edgeJobsView', r2a(withUIRouter(withCurrentUser(ListView)), []))
|
||||||
.component(
|
.component(
|
||||||
'edgeJobsView',
|
'edgeJobsCreateView',
|
||||||
r2a(withUIRouter(withCurrentUser(ListView)), [])
|
r2a(withUIRouter(withCurrentUser(CreateView)), [])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -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>
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import { CreateEdgeJobViewController } from './createEdgeJobViewController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('createEdgeJobView', {
|
|
||||||
templateUrl: './createEdgeJobView.html',
|
|
||||||
controller: CreateEdgeJobViewController,
|
|
||||||
});
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -59,6 +59,7 @@ interface Props extends AutomationTestingProps {
|
||||||
id: string;
|
id: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
yaml?: boolean;
|
yaml?: boolean;
|
||||||
|
shell?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
titleContent?: React.ReactNode;
|
titleContent?: React.ReactNode;
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
|
@ -77,6 +78,7 @@ export function WebEditorForm({
|
||||||
hideTitle,
|
hideTitle,
|
||||||
readonly,
|
readonly,
|
||||||
yaml,
|
yaml,
|
||||||
|
shell,
|
||||||
children,
|
children,
|
||||||
error,
|
error,
|
||||||
versions,
|
versions,
|
||||||
|
@ -108,6 +110,7 @@ export function WebEditorForm({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
yaml={yaml}
|
yaml={yaml}
|
||||||
|
shell={shell}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
versions={versions}
|
versions={versions}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface Option<T extends string | number>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<T extends string | number> extends AutomationTestingProps {
|
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>({
|
export function Select<T extends number | string>({
|
||||||
|
|
|
@ -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(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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]
|
||||||
|
);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { formatDate } from '@/portainer/filters/filters';
|
import { formatDate } from '@/portainer/filters/filters';
|
||||||
|
|
||||||
import { FORMAT } from '../../common/ScheduledTimeField';
|
import { FORMAT } from '@@/DateTimeField';
|
||||||
|
|
||||||
import { columnHelper } from './helper';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import DateTimePicker from 'react-datetime-picker';
|
import { useField } from 'formik';
|
||||||
import { Calendar, X } from 'lucide-react';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { string } from 'yup';
|
import { string } from 'yup';
|
||||||
import { useField } from 'formik';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isoDate,
|
isoDate,
|
||||||
|
@ -10,25 +8,18 @@ import {
|
||||||
TIME_FORMAT,
|
TIME_FORMAT,
|
||||||
} from '@/portainer/filters/filters';
|
} from '@/portainer/filters/filters';
|
||||||
|
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { DateTimeField, FORMAT } from '@@/DateTimeField';
|
||||||
import { Input } from '@@/form-components/Input';
|
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
import { FormValues } from './types';
|
import { FormValues } from './types';
|
||||||
|
|
||||||
import 'react-datetime-picker/dist/DateTimePicker.css';
|
|
||||||
import 'react-calendar/dist/Calendar.css';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FORMAT = 'YYYY-MM-DD HH:mm';
|
|
||||||
|
|
||||||
export function ScheduledTimeField({ disabled }: Props) {
|
export function ScheduledTimeField({ disabled }: Props) {
|
||||||
const [{ name, value }, { error }, { setValue }] =
|
const [{ name, value }, { error }, { setValue }] =
|
||||||
useField<FormValues['scheduledTime']>('scheduledTime');
|
useField<FormValues['scheduledTime']>('scheduledTime');
|
||||||
|
|
||||||
const dateValue = useMemo(() => parseIsoDate(value, FORMAT), [value]);
|
const dateValue = useMemo(() => parseIsoDate(value, FORMAT), [value]);
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -37,31 +28,19 @@ export function ScheduledTimeField({ disabled }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormControl label="Schedule date & time" errors={error}>
|
<DateTimeField
|
||||||
{!disabled ? (
|
label="Schedule date & time"
|
||||||
<DateTimePicker
|
minDate={new Date(Date.now() - 24 * 60 * 60 * 1000)}
|
||||||
format="y-MM-dd HH:mm"
|
|
||||||
className="form-control [&>div]:border-0"
|
|
||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
const dateToSave =
|
const dateToSave = date || new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||||
date || new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
||||||
setValue(isoDate(dateToSave.valueOf(), FORMAT));
|
setValue(isoDate(dateToSave.valueOf(), FORMAT));
|
||||||
}}
|
}}
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
name={name}
|
name={name}
|
||||||
value={dateValue}
|
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"
|
data-cy="update-schedules-time-input"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
{!disabled && value && (
|
{!disabled && value && (
|
||||||
<TextTip color="blue">
|
<TextTip color="blue">
|
||||||
If time zone is not set on edge agent then UTC+0 will be used.
|
If time zone is not set on edge agent then UTC+0 will be used.
|
||||||
|
|
Loading…
Reference in New Issue