mirror of
https://github.com/ZwareBear/awx.git
synced 2026-04-24 08:51:48 -05:00
Merge ui and ui_next in one dir
Merge ui and ui_next in one dir See: https://github.com/ansible/awx/issues/10676 Update django .po files Update django .po files Run `awx-manage makemessages`.
This commit is contained in:
650
awx/ui/src/components/Schedule/shared/ScheduleForm.js
Normal file
650
awx/ui/src/components/Schedule/shared/ScheduleForm.js
Normal file
@@ -0,0 +1,650 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { shape, func } from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { RRule } from 'rrule';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Title,
|
||||
ActionGroup,
|
||||
} from '@patternfly/react-core';
|
||||
import { Config } from 'contexts/Config';
|
||||
import { SchedulesAPI } from 'api';
|
||||
import { dateToInputDateTime } from 'util/dates';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import { required } from 'util/validators';
|
||||
import { parseVariableField } from 'util/yaml';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import ContentError from '../../ContentError';
|
||||
import ContentLoading from '../../ContentLoading';
|
||||
import FormField, { FormSubmitError } from '../../FormField';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
SubFormLayout,
|
||||
FormFullWidthLayout,
|
||||
} from '../../FormLayout';
|
||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||
import SchedulePromptableFields from './SchedulePromptableFields';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
|
||||
const generateRunOnTheDay = (days = []) => {
|
||||
if (
|
||||
[
|
||||
RRule.MO,
|
||||
RRule.TU,
|
||||
RRule.WE,
|
||||
RRule.TH,
|
||||
RRule.FR,
|
||||
RRule.SA,
|
||||
RRule.SU,
|
||||
].every((element) => days.indexOf(element) > -1)
|
||||
) {
|
||||
return 'day';
|
||||
}
|
||||
if (
|
||||
[RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every(
|
||||
(element) => days.indexOf(element) > -1
|
||||
)
|
||||
) {
|
||||
return 'weekday';
|
||||
}
|
||||
if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) {
|
||||
return 'weekendDay';
|
||||
}
|
||||
if (days.indexOf(RRule.MO) > -1) {
|
||||
return 'monday';
|
||||
}
|
||||
if (days.indexOf(RRule.TU) > -1) {
|
||||
return 'tuesday';
|
||||
}
|
||||
if (days.indexOf(RRule.WE) > -1) {
|
||||
return 'wednesday';
|
||||
}
|
||||
if (days.indexOf(RRule.TH) > -1) {
|
||||
return 'thursday';
|
||||
}
|
||||
if (days.indexOf(RRule.FR) > -1) {
|
||||
return 'friday';
|
||||
}
|
||||
if (days.indexOf(RRule.SA) > -1) {
|
||||
return 'saturday';
|
||||
}
|
||||
if (days.indexOf(RRule.SU) > -1) {
|
||||
return 'sunday';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) {
|
||||
const [timezone, timezoneMeta] = useField({
|
||||
name: 'timezone',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [frequency, frequencyMeta] = useField({
|
||||
name: 'frequency',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
id="schedule-name"
|
||||
label={t`Name`}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="schedule-description"
|
||||
label={t`Description`}
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<DateTimePicker
|
||||
dateFieldName="startDate"
|
||||
timeFieldName="startTime"
|
||||
label={t`Start`}
|
||||
/>
|
||||
<FormGroup
|
||||
name="timezone"
|
||||
fieldId="schedule-timezone"
|
||||
helperTextInvalid={timezoneMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
!timezoneMeta.touched || !timezoneMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={t`Local time zone`}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-timezone"
|
||||
data={zoneOptions}
|
||||
{...timezone}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
name="frequency"
|
||||
fieldId="schedule-requency"
|
||||
helperTextInvalid={frequencyMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
!frequencyMeta.touched || !frequencyMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={t`Run frequency`}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-frequency"
|
||||
data={[
|
||||
{ value: 'none', key: 'none', label: t`None (run once)` },
|
||||
{ value: 'minute', key: 'minute', label: t`Minute` },
|
||||
{ value: 'hour', key: 'hour', label: t`Hour` },
|
||||
{ value: 'day', key: 'day', label: t`Day` },
|
||||
{ value: 'week', key: 'week', label: t`Week` },
|
||||
{ value: 'month', key: 'month', label: t`Month` },
|
||||
{ value: 'year', key: 'year', label: t`Year` },
|
||||
]}
|
||||
{...frequency}
|
||||
/>
|
||||
</FormGroup>
|
||||
{hasDaysToKeepField ? (
|
||||
<FormField
|
||||
id="schedule-days-to-keep"
|
||||
label={t`Days of Data to Keep`}
|
||||
name="daysToKeep"
|
||||
type="number"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
) : null}
|
||||
{frequency.value !== 'none' && (
|
||||
<SubFormLayout>
|
||||
<Title size="md" headingLevel="h4">
|
||||
{t`Frequency Details`}
|
||||
</Title>
|
||||
<FormColumnLayout>
|
||||
<FrequencyDetailSubform />
|
||||
</FormColumnLayout>
|
||||
</SubFormLayout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduleForm({
|
||||
hasDaysToKeepField,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
schedule,
|
||||
submitError,
|
||||
resource,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resourceDefaultCredentials,
|
||||
...rest
|
||||
}) {
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||
|
||||
let rruleError;
|
||||
const now = new Date();
|
||||
const closestQuarterHour = new Date(
|
||||
Math.ceil(now.getTime() / 900000) * 900000
|
||||
);
|
||||
const tomorrow = new Date(closestQuarterHour);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const isTemplate =
|
||||
resource.type === 'workflow_job_template' ||
|
||||
resource.type === 'job_template';
|
||||
const {
|
||||
request: loadScheduleData,
|
||||
error: contentError,
|
||||
isLoading: contentLoading,
|
||||
result: { zoneOptions, credentials },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await SchedulesAPI.readZoneInfo();
|
||||
|
||||
let creds;
|
||||
if (schedule.id) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await SchedulesAPI.readCredentials(schedule.id);
|
||||
creds = results;
|
||||
}
|
||||
|
||||
const zones = data.map((zone) => ({
|
||||
value: zone.name,
|
||||
key: zone.name,
|
||||
label: zone.name,
|
||||
}));
|
||||
|
||||
return {
|
||||
zoneOptions: zones,
|
||||
credentials: creds || [],
|
||||
};
|
||||
}, [schedule]),
|
||||
{
|
||||
zonesOptions: [],
|
||||
credentials: [],
|
||||
isLoading: true,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadScheduleData();
|
||||
}, [loadScheduleData]);
|
||||
|
||||
const missingRequiredInventory = useCallback(() => {
|
||||
let missingInventory = false;
|
||||
if (
|
||||
launchConfig.inventory_needed_to_start &&
|
||||
!schedule?.summary_fields?.inventory?.id
|
||||
) {
|
||||
missingInventory = true;
|
||||
}
|
||||
return missingInventory;
|
||||
}, [launchConfig, schedule]);
|
||||
|
||||
const hasMissingSurveyValue = useCallback(() => {
|
||||
let missingValues = false;
|
||||
if (launchConfig?.survey_enabled) {
|
||||
surveyConfig.spec.forEach((question) => {
|
||||
const hasDefaultValue = Boolean(question.default);
|
||||
const hasSchedule = Object.keys(schedule).length;
|
||||
const isRequired = question.required;
|
||||
if (isRequired && !hasDefaultValue) {
|
||||
if (!hasSchedule) {
|
||||
missingValues = true;
|
||||
} else {
|
||||
const hasMatchingKey = Object.keys(schedule?.extra_data).includes(
|
||||
question.variable
|
||||
);
|
||||
Object.values(schedule?.extra_data).forEach((value) => {
|
||||
if (!value || !hasMatchingKey) {
|
||||
missingValues = true;
|
||||
} else {
|
||||
missingValues = false;
|
||||
}
|
||||
});
|
||||
if (!Object.values(schedule.extra_data).length) {
|
||||
missingValues = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return missingValues;
|
||||
}, [launchConfig, schedule, surveyConfig]);
|
||||
|
||||
const hasCredentialsThatPrompt = useCallback(() => {
|
||||
if (launchConfig?.ask_credential_on_launch) {
|
||||
if (Object.keys(schedule).length > 0) {
|
||||
const defaultCredsWithoutOverrides = [];
|
||||
|
||||
const credentialHasOverride = (templateDefaultCred) => {
|
||||
let hasOverride = false;
|
||||
credentials.forEach((nodeCredential) => {
|
||||
if (
|
||||
templateDefaultCred.credential_type ===
|
||||
nodeCredential.credential_type
|
||||
) {
|
||||
if (
|
||||
(!templateDefaultCred.vault_id &&
|
||||
!nodeCredential.inputs.vault_id) ||
|
||||
(templateDefaultCred.vault_id &&
|
||||
nodeCredential.inputs.vault_id &&
|
||||
templateDefaultCred.vault_id ===
|
||||
nodeCredential.inputs.vault_id)
|
||||
) {
|
||||
hasOverride = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hasOverride;
|
||||
};
|
||||
|
||||
if (resourceDefaultCredentials) {
|
||||
resourceDefaultCredentials.forEach((defaultCred) => {
|
||||
if (!credentialHasOverride(defaultCred)) {
|
||||
defaultCredsWithoutOverrides.push(defaultCred);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
credentials
|
||||
.concat(defaultCredsWithoutOverrides)
|
||||
.filter((credential) => {
|
||||
let credentialRequiresPass = false;
|
||||
|
||||
Object.entries(credential.inputs).forEach(([key, value]) => {
|
||||
if (key !== 'vault_id' && value === 'ASK') {
|
||||
credentialRequiresPass = true;
|
||||
}
|
||||
});
|
||||
|
||||
return credentialRequiresPass;
|
||||
}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
return launchConfig?.defaults?.credentials
|
||||
? launchConfig.defaults.credentials.filter(
|
||||
(credential) => credential?.passwords_needed.length > 0
|
||||
).length > 0
|
||||
: false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [launchConfig, schedule, credentials, resourceDefaultCredentials]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isTemplate &&
|
||||
(missingRequiredInventory() ||
|
||||
hasMissingSurveyValue() ||
|
||||
hasCredentialsThatPrompt())
|
||||
) {
|
||||
setIsSaveDisabled(true);
|
||||
}
|
||||
}, [
|
||||
isTemplate,
|
||||
hasMissingSurveyValue,
|
||||
missingRequiredInventory,
|
||||
hasCredentialsThatPrompt,
|
||||
]);
|
||||
|
||||
let showPromptButton = false;
|
||||
|
||||
if (
|
||||
launchConfig &&
|
||||
(launchConfig.ask_inventory_on_launch ||
|
||||
launchConfig.ask_variables_on_launch ||
|
||||
launchConfig.ask_job_type_on_launch ||
|
||||
launchConfig.ask_limit_on_launch ||
|
||||
launchConfig.ask_credential_on_launch ||
|
||||
launchConfig.ask_scm_branch_on_launch ||
|
||||
launchConfig.survey_enabled ||
|
||||
launchConfig.inventory_needed_to_start ||
|
||||
launchConfig.variables_needed_to_start?.length > 0)
|
||||
) {
|
||||
showPromptButton = true;
|
||||
}
|
||||
const [currentDate, time] = dateToInputDateTime(closestQuarterHour);
|
||||
|
||||
const [tomorrowDate] = dateToInputDateTime(tomorrow);
|
||||
const initialValues = {
|
||||
daysOfWeek: [],
|
||||
description: schedule.description || '',
|
||||
end: 'never',
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
name: schedule.name || '',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
startDate: currentDate,
|
||||
startTime: time,
|
||||
timezone: schedule.timezone || 'America/New_York',
|
||||
};
|
||||
const submitSchedule = (
|
||||
values,
|
||||
launchConfiguration,
|
||||
surveyConfiguration,
|
||||
scheduleCredentials
|
||||
) => {
|
||||
handleSubmit(
|
||||
values,
|
||||
launchConfiguration,
|
||||
surveyConfiguration,
|
||||
scheduleCredentials
|
||||
);
|
||||
};
|
||||
|
||||
if (hasDaysToKeepField) {
|
||||
let initialDaysToKeep = 30;
|
||||
if (schedule?.extra_data) {
|
||||
if (
|
||||
typeof schedule?.extra_data === 'string' &&
|
||||
schedule?.extra_data !== ''
|
||||
) {
|
||||
initialDaysToKeep = parseVariableField(schedule?.extra_data).days;
|
||||
}
|
||||
if (typeof schedule?.extra_data === 'object') {
|
||||
initialDaysToKeep = schedule?.extra_data?.days;
|
||||
}
|
||||
}
|
||||
initialValues.daysToKeep = initialDaysToKeep;
|
||||
}
|
||||
|
||||
const overriddenValues = {};
|
||||
|
||||
if (Object.keys(schedule).length > 0) {
|
||||
if (schedule.rrule) {
|
||||
try {
|
||||
const {
|
||||
origOptions: {
|
||||
bymonth,
|
||||
bymonthday,
|
||||
bysetpos,
|
||||
byweekday,
|
||||
count,
|
||||
dtstart,
|
||||
freq,
|
||||
interval,
|
||||
},
|
||||
} = RRule.fromString(schedule.rrule.replace(' ', '\n'));
|
||||
|
||||
if (dtstart) {
|
||||
const [startDate, startTime] = dateToInputDateTime(schedule.dtstart);
|
||||
|
||||
overriddenValues.startDate = startDate;
|
||||
overriddenValues.startTime = startTime;
|
||||
}
|
||||
|
||||
if (schedule.until) {
|
||||
overriddenValues.end = 'onDate';
|
||||
|
||||
const [endDate, endTime] = dateToInputDateTime(schedule.until);
|
||||
|
||||
overriddenValues.endDate = endDate;
|
||||
overriddenValues.endTime = endTime;
|
||||
} else if (count) {
|
||||
overriddenValues.end = 'after';
|
||||
overriddenValues.occurrences = count;
|
||||
}
|
||||
|
||||
if (interval) {
|
||||
overriddenValues.interval = interval;
|
||||
}
|
||||
|
||||
if (typeof freq === 'number') {
|
||||
switch (freq) {
|
||||
case RRule.MINUTELY:
|
||||
if (schedule.dtstart !== schedule.dtend) {
|
||||
overriddenValues.frequency = 'minute';
|
||||
}
|
||||
break;
|
||||
case RRule.HOURLY:
|
||||
overriddenValues.frequency = 'hour';
|
||||
break;
|
||||
case RRule.DAILY:
|
||||
overriddenValues.frequency = 'day';
|
||||
break;
|
||||
case RRule.WEEKLY:
|
||||
overriddenValues.frequency = 'week';
|
||||
if (byweekday) {
|
||||
overriddenValues.daysOfWeek = byweekday;
|
||||
}
|
||||
break;
|
||||
case RRule.MONTHLY:
|
||||
overriddenValues.frequency = 'month';
|
||||
if (bymonthday) {
|
||||
overriddenValues.runOnDayNumber = bymonthday;
|
||||
} else if (bysetpos) {
|
||||
overriddenValues.runOn = 'the';
|
||||
overriddenValues.runOnTheOccurrence = bysetpos;
|
||||
overriddenValues.runOnTheDay = generateRunOnTheDay(byweekday);
|
||||
}
|
||||
break;
|
||||
case RRule.YEARLY:
|
||||
overriddenValues.frequency = 'year';
|
||||
if (bymonthday) {
|
||||
overriddenValues.runOnDayNumber = bymonthday;
|
||||
overriddenValues.runOnDayMonth = bymonth;
|
||||
} else if (bysetpos) {
|
||||
overriddenValues.runOn = 'the';
|
||||
overriddenValues.runOnTheOccurrence = bysetpos;
|
||||
overriddenValues.runOnTheDay = generateRunOnTheDay(byweekday);
|
||||
overriddenValues.runOnTheMonth = bymonth;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
rruleError = error;
|
||||
}
|
||||
} else {
|
||||
rruleError = new Error(t`Schedule is missing rrule`);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentError || rruleError) {
|
||||
return <ContentError error={contentError || rruleError} />;
|
||||
}
|
||||
|
||||
if (contentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Config>
|
||||
{() => (
|
||||
<Formik
|
||||
initialValues={Object.assign(initialValues, overriddenValues)}
|
||||
onSubmit={(values) => {
|
||||
submitSchedule(values, launchConfig, surveyConfig, credentials);
|
||||
}}
|
||||
validate={(values) => {
|
||||
const errors = {};
|
||||
const {
|
||||
end,
|
||||
endDate,
|
||||
frequency,
|
||||
runOn,
|
||||
runOnDayNumber,
|
||||
startDate,
|
||||
} = values;
|
||||
|
||||
if (end === 'onDate' && new Date(startDate) >= new Date(endDate)) {
|
||||
errors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||
}
|
||||
|
||||
if (
|
||||
(frequency === 'month' || frequency === 'year') &&
|
||||
runOn === 'day' &&
|
||||
(runOnDayNumber < 1 || runOnDayNumber > 31)
|
||||
) {
|
||||
errors.runOn = t`Please select a day number between 1 and 31.`;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<ScheduleFormFields
|
||||
hasDaysToKeepField={hasDaysToKeepField}
|
||||
zoneOptions={zoneOptions}
|
||||
{...rest}
|
||||
/>
|
||||
{isWizardOpen && (
|
||||
<SchedulePromptableFields
|
||||
schedule={schedule}
|
||||
credentials={credentials}
|
||||
surveyConfig={surveyConfig}
|
||||
launchConfig={launchConfig}
|
||||
resource={resource}
|
||||
onCloseWizard={() => {
|
||||
setIsWizardOpen(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(false);
|
||||
}}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
)}
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormFullWidthLayout>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
ouiaId="schedule-form-save-button"
|
||||
aria-label={t`Save`}
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={formik.handleSubmit}
|
||||
isDisabled={isSaveDisabled}
|
||||
>
|
||||
{t`Save`}
|
||||
</Button>
|
||||
|
||||
{isTemplate && showPromptButton && (
|
||||
<Button
|
||||
ouiaId="schedule-form-prompt-button"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
aria-label={t`Prompt`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
>
|
||||
{t`Prompt`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
ouiaId="schedule-form-cancel-button"
|
||||
aria-label={t`Cancel`}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormFullWidthLayout>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</Config>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleForm.propTypes = {
|
||||
handleCancel: func.isRequired,
|
||||
handleSubmit: func.isRequired,
|
||||
schedule: shape({}),
|
||||
submitError: shape(),
|
||||
};
|
||||
|
||||
ScheduleForm.defaultProps = {
|
||||
schedule: {},
|
||||
submitError: null,
|
||||
};
|
||||
|
||||
export default ScheduleForm;
|
||||
Reference in New Issue
Block a user