mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 07:26:03 -03:30
Merge pull request #8235 from AlexSCorey/5913-RefactorJTPOL
Restructures Job Template POL and renames useSteps Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -2,32 +2,27 @@ import React from 'react';
|
|||||||
import { Wizard } from '@patternfly/react-core';
|
import { Wizard } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Formik } from 'formik';
|
import { Formik, useFormikContext } from 'formik';
|
||||||
import ContentError from '../ContentError';
|
import ContentError from '../ContentError';
|
||||||
import ContentLoading from '../ContentLoading';
|
import ContentLoading from '../ContentLoading';
|
||||||
|
import { useDismissableError } from '../../util/useRequest';
|
||||||
import mergeExtraVars from './mergeExtraVars';
|
import mergeExtraVars from './mergeExtraVars';
|
||||||
import useSteps from './useSteps';
|
import useLaunchSteps from './useLaunchSteps';
|
||||||
|
import AlertModal from '../AlertModal';
|
||||||
import getSurveyValues from './getSurveyValues';
|
import getSurveyValues from './getSurveyValues';
|
||||||
|
|
||||||
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
||||||
|
const { values, setTouched, validateForm } = useFormikContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
steps,
|
steps,
|
||||||
initialValues,
|
|
||||||
isReady,
|
isReady,
|
||||||
validate,
|
|
||||||
visitStep,
|
visitStep,
|
||||||
visitAllSteps,
|
visitAllSteps,
|
||||||
contentError,
|
contentError,
|
||||||
} = useSteps(config, resource, i18n);
|
} = useLaunchSteps(config, resource, i18n);
|
||||||
|
|
||||||
if (contentError) {
|
const handleSave = () => {
|
||||||
return <ContentError error={contentError} />;
|
|
||||||
}
|
|
||||||
if (!isReady) {
|
|
||||||
return <ContentLoading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = values => {
|
|
||||||
const postValues = {};
|
const postValues = {};
|
||||||
const setValue = (key, value) => {
|
const setValue = (key, value) => {
|
||||||
if (typeof value !== 'undefined' && value !== null) {
|
if (typeof value !== 'undefined' && value !== null) {
|
||||||
@@ -49,39 +44,89 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
|||||||
: resource.extra_vars;
|
: resource.extra_vars;
|
||||||
setValue('extra_vars', mergeExtraVars(extraVars, surveyValues));
|
setValue('extra_vars', mergeExtraVars(extraVars, surveyValues));
|
||||||
setValue('scm_branch', values.scm_branch);
|
setValue('scm_branch', values.scm_branch);
|
||||||
onLaunch(postValues);
|
|
||||||
|
onSubmit(postValues);
|
||||||
};
|
};
|
||||||
|
const { error, dismissError } = useDismissableError(contentError);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => {
|
||||||
|
dismissError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentError error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={initialValues} onSubmit={submit} validate={validate}>
|
<Wizard
|
||||||
{({ validateForm, setTouched, handleSubmit }) => (
|
isOpen
|
||||||
<Wizard
|
onClose={onCancel}
|
||||||
isOpen
|
onSave={handleSave}
|
||||||
onClose={onCancel}
|
onNext={async (nextStep, prevStep) => {
|
||||||
onSave={handleSubmit}
|
if (nextStep.id === 'preview') {
|
||||||
onNext={async (nextStep, prevStep) => {
|
visitAllSteps(setTouched);
|
||||||
if (nextStep.id === 'preview') {
|
} else {
|
||||||
visitAllSteps(setTouched);
|
visitStep(prevStep.prevId);
|
||||||
} else {
|
}
|
||||||
visitStep(prevStep.prevId);
|
await validateForm();
|
||||||
}
|
}}
|
||||||
await validateForm();
|
onGoToStep={async (nextStep, prevStep) => {
|
||||||
}}
|
if (nextStep.id === 'preview') {
|
||||||
onGoToStep={async (newStep, prevStep) => {
|
visitAllSteps(setTouched);
|
||||||
if (newStep.id === 'preview') {
|
} else {
|
||||||
visitAllSteps(setTouched);
|
visitStep(prevStep.prevId);
|
||||||
} else {
|
}
|
||||||
visitStep(prevStep.prevId);
|
await validateForm();
|
||||||
}
|
}}
|
||||||
await validateForm();
|
title={i18n._(t`Prompts`)}
|
||||||
}}
|
steps={
|
||||||
title={i18n._(t`Prompts`)}
|
isReady
|
||||||
steps={steps}
|
? steps
|
||||||
backButtonText={i18n._(t`Back`)}
|
: [
|
||||||
cancelButtonText={i18n._(t`Cancel`)}
|
{
|
||||||
nextButtonText={i18n._(t`Next`)}
|
name: i18n._(t`Content Loading`),
|
||||||
/>
|
component: <ContentLoading />,
|
||||||
)}
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
backButtonText={i18n._(t`Back`)}
|
||||||
|
cancelButtonText={i18n._(t`Cancel`)}
|
||||||
|
nextButtonText={i18n._(t`Next`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LaunchPrompt({ config, resource = {}, onLaunch, onCancel, i18n }) {
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
verbosity: resource.verbosity || 0,
|
||||||
|
inventory: resource.summary_fields?.inventory || null,
|
||||||
|
credentials: resource.summary_fields?.credentials || null,
|
||||||
|
diff_mode: resource.diff_mode || false,
|
||||||
|
extra_vars: resource.extra_vars || '---',
|
||||||
|
job_type: resource.job_type || '',
|
||||||
|
job_tags: resource.job_tags || '',
|
||||||
|
skip_tags: resource.skip_tags || '',
|
||||||
|
scm_branch: resource.scm_branch || '',
|
||||||
|
limit: resource.limit || '',
|
||||||
|
}}
|
||||||
|
onSubmit={values => onLaunch(values)}
|
||||||
|
>
|
||||||
|
<PromptModalForm
|
||||||
|
onSubmit={values => onLaunch(values)}
|
||||||
|
onCancel={onCancel}
|
||||||
|
i18n={i18n}
|
||||||
|
config={config}
|
||||||
|
resource={resource}
|
||||||
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ describe('LaunchPrompt', () => {
|
|||||||
expect(steps).toHaveLength(5);
|
expect(steps).toHaveLength(5);
|
||||||
expect(steps[0].name.props.children).toEqual('Inventory');
|
expect(steps[0].name.props.children).toEqual('Inventory');
|
||||||
expect(steps[1].name).toEqual('Credentials');
|
expect(steps[1].name).toEqual('Credentials');
|
||||||
expect(steps[2].name.props.children).toEqual('Other Prompts');
|
expect(steps[2].name).toEqual('Other Prompts');
|
||||||
expect(steps[3].name.props.children).toEqual('Survey');
|
expect(steps[3].name.props.children).toEqual('Survey');
|
||||||
expect(steps[4].name).toEqual('Preview');
|
expect(steps[4].name).toEqual('Preview');
|
||||||
});
|
});
|
||||||
@@ -167,7 +167,7 @@ describe('LaunchPrompt', () => {
|
|||||||
const steps = wizard.prop('steps');
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(2);
|
expect(steps).toHaveLength(2);
|
||||||
expect(steps[0].name.props.children).toEqual('Other Prompts');
|
expect(steps[0].name).toEqual('Other Prompts');
|
||||||
expect(isElementOfType(steps[0].component, OtherPromptsStep)).toEqual(true);
|
expect(isElementOfType(steps[0].component, OtherPromptsStep)).toEqual(true);
|
||||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
export default function getSurveyValues(values) {
|
export default function getSurveyValues(values) {
|
||||||
const surveyValues = {};
|
const surveyValues = {};
|
||||||
Object.keys(values).forEach(key => {
|
Object.keys(values).forEach(key => {
|
||||||
if (key.startsWith('survey_')) {
|
if (key.startsWith('survey_') && values[key] !== []) {
|
||||||
|
if (Array.isArray(values[key]) && values[key].length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
surveyValues[key.substr(7)] = values[key];
|
surveyValues[key.substr(7)] = values[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function OtherPromptsStep({ config, i18n }) {
|
|||||||
id="prompt-job-tags"
|
id="prompt-job-tags"
|
||||||
name="job_tags"
|
name="job_tags"
|
||||||
label={i18n._(t`Job Tags`)}
|
label={i18n._(t`Job Tags`)}
|
||||||
|
aria-label={i18n._(t`Job Tags`)}
|
||||||
tooltip={i18n._(t`Tags are useful when you have a large
|
tooltip={i18n._(t`Tags are useful when you have a large
|
||||||
playbook, and you want to run a specific part of a play or task.
|
playbook, and you want to run a specific part of a play or task.
|
||||||
Use commas to separate multiple tags. Refer to Ansible Tower
|
Use commas to separate multiple tags. Refer to Ansible Tower
|
||||||
@@ -62,6 +63,7 @@ function OtherPromptsStep({ config, i18n }) {
|
|||||||
id="prompt-skip-tags"
|
id="prompt-skip-tags"
|
||||||
name="skip_tags"
|
name="skip_tags"
|
||||||
label={i18n._(t`Skip Tags`)}
|
label={i18n._(t`Skip Tags`)}
|
||||||
|
aria-label={i18n._(t`Skip Tags`)}
|
||||||
tooltip={i18n._(t`Skip tags are useful when you have a large
|
tooltip={i18n._(t`Skip tags are useful when you have a large
|
||||||
playbook, and you want to skip specific parts of a play or task.
|
playbook, and you want to skip specific parts of a play or task.
|
||||||
Use commas to separate multiple tags. Refer to Ansible Tower
|
Use commas to separate multiple tags. Refer to Ansible Tower
|
||||||
@@ -108,6 +110,7 @@ function JobTypeField({ i18n }) {
|
|||||||
and report problems without executing the playbook.`)}
|
and report problems without executing the playbook.`)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
isRequired
|
||||||
validated={isValid ? 'default' : 'error'}
|
validated={isValid ? 'default' : 'error'}
|
||||||
>
|
>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
@@ -129,6 +132,7 @@ function VerbosityField({ i18n }) {
|
|||||||
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
||||||
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
||||||
];
|
];
|
||||||
|
|
||||||
const isValid = !(meta.touched && meta.error);
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,6 +175,7 @@ function ShowChangesToggle({ i18n }) {
|
|||||||
</label>
|
</label>
|
||||||
</FieldHeader>
|
</FieldHeader>
|
||||||
<Switch
|
<Switch
|
||||||
|
aria-label={field.value ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||||
id="prompt-show-changes"
|
id="prompt-show-changes"
|
||||||
label={i18n._(t`On`)}
|
label={i18n._(t`On`)}
|
||||||
labelOff={i18n._(t`Off`)}
|
labelOff={i18n._(t`Off`)}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{formErrors.length > 0 && (
|
{formErrors && (
|
||||||
<ErrorMessageWrapper>
|
<ErrorMessageWrapper>
|
||||||
{i18n._(t`Some of the previous step(s) have errors`)}
|
{i18n._(t`Some of the previous step(s) have errors`)}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -104,4 +104,31 @@ describe('PreviewStep', () => {
|
|||||||
extra_vars: 'one: 1',
|
extra_vars: 'one: 1',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should remove survey with empty array value', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{ extra_vars: 'one: 1' }}
|
||||||
|
values={{ extra_vars: 'one: 1', survey_foo: [] }}
|
||||||
|
>
|
||||||
|
<PreviewStep
|
||||||
|
resource={resource}
|
||||||
|
config={{
|
||||||
|
ask_variables_on_launch: true,
|
||||||
|
}}
|
||||||
|
formErrors={formErrors}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const detail = wrapper.find('PromptDetail');
|
||||||
|
expect(detail).toHaveLength(1);
|
||||||
|
expect(detail.prop('resource')).toEqual(resource);
|
||||||
|
expect(detail.prop('overrides')).toEqual({
|
||||||
|
extra_vars: 'one: 1',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -114,15 +115,22 @@ function MultipleChoiceField({ question }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MultiSelectField({ question }) {
|
function MultiSelectField({ question, i18n }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [field, meta, helpers] = useField(`survey_${question.variable}`);
|
const [field, meta, helpers] = useField({
|
||||||
|
name: `survey_${question.variable}`,
|
||||||
|
validate: question.isrequired ? required(null, i18n) : null,
|
||||||
|
});
|
||||||
const id = `survey-question-${question.variable}`;
|
const id = `survey-question-${question.variable}`;
|
||||||
const isValid = !(meta.touched && meta.error);
|
const hasActualValue = !question.required || meta.value.length > 0;
|
||||||
|
const isValid = !meta.touched || (!meta.error && hasActualValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId={id}
|
fieldId={id}
|
||||||
helperTextInvalid={meta.error}
|
helperTextInvalid={
|
||||||
|
meta.error || i18n._(t`Must select a value for this field.`)
|
||||||
|
}
|
||||||
isRequired={question.required}
|
isRequired={question.required}
|
||||||
validated={isValid ? 'default' : 'error'}
|
validated={isValid ? 'default' : 'error'}
|
||||||
label={question.question_name}
|
label={question.question_name}
|
||||||
@@ -133,14 +141,19 @@ function MultiSelectField({ question }) {
|
|||||||
id={id}
|
id={id}
|
||||||
onToggle={setIsOpen}
|
onToggle={setIsOpen}
|
||||||
onSelect={(event, option) => {
|
onSelect={(event, option) => {
|
||||||
if (field.value.includes(option)) {
|
if (field?.value?.includes(option)) {
|
||||||
helpers.setValue(field.value.filter(o => o !== option));
|
helpers.setValue(field.value.filter(o => o !== option));
|
||||||
} else {
|
} else {
|
||||||
helpers.setValue(field.value.concat(option));
|
helpers.setValue(field.value.concat(option));
|
||||||
}
|
}
|
||||||
|
helpers.setTouched(true);
|
||||||
}}
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
selections={field.value}
|
selections={field.value}
|
||||||
|
onClear={() => {
|
||||||
|
helpers.setTouched(true);
|
||||||
|
helpers.setValue([]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{question.choices.split('\n').map(opt => (
|
{question.choices.split('\n').map(opt => (
|
||||||
<SelectOption key={opt} value={opt} />
|
<SelectOption key={opt} value={opt} />
|
||||||
|
|||||||
@@ -4,20 +4,9 @@ import CredentialsStep from './CredentialsStep';
|
|||||||
|
|
||||||
const STEP_ID = 'credentials';
|
const STEP_ID = 'credentials';
|
||||||
|
|
||||||
export default function useCredentialsStep(
|
export default function useCredentialsStep(config, i18n) {
|
||||||
config,
|
|
||||||
resource,
|
|
||||||
visitedSteps,
|
|
||||||
i18n
|
|
||||||
) {
|
|
||||||
const validate = () => {
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(config, i18n),
|
step: getStep(config, i18n),
|
||||||
initialValues: getInitialValues(config, resource),
|
|
||||||
validate,
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
formError: null,
|
||||||
@@ -39,12 +28,3 @@ function getStep(config, i18n) {
|
|||||||
component: <CredentialsStep i18n={i18n} />,
|
component: <CredentialsStep i18n={i18n} />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialValues(config, resource) {
|
|
||||||
if (!config.ask_credential_on_launch) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
credentials: resource?.summary_fields?.credentials || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
import InventoryStep from './InventoryStep';
|
import InventoryStep from './InventoryStep';
|
||||||
import StepName from './StepName';
|
import StepName from './StepName';
|
||||||
|
|
||||||
const STEP_ID = 'inventory';
|
const STEP_ID = 'inventory';
|
||||||
|
|
||||||
export default function useInventoryStep(config, resource, visitedSteps, i18n) {
|
export default function useInventoryStep(config, visitedSteps, i18n) {
|
||||||
const [stepErrors, setStepErrors] = useState({});
|
const [, meta] = useField('inventory');
|
||||||
|
|
||||||
const validate = values => {
|
|
||||||
if (
|
|
||||||
!config.ask_inventory_on_launch ||
|
|
||||||
(['workflow_job', 'workflow_job_template'].includes(resource.type) &&
|
|
||||||
!resource.inventory)
|
|
||||||
) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const errors = {};
|
|
||||||
if (!values.inventory) {
|
|
||||||
errors.inventory = i18n._(t`An inventory must be selected`);
|
|
||||||
}
|
|
||||||
setStepErrors(errors);
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(config, hasErrors, i18n),
|
step: getStep(config, meta, i18n, visitedSteps),
|
||||||
initialValues: getInitialValues(config, resource),
|
|
||||||
validate,
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: stepErrors,
|
formError: !meta.value,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldsTouched => {
|
||||||
setFieldsTouched({
|
setFieldsTouched({
|
||||||
inventory: true,
|
inventory: true,
|
||||||
@@ -40,23 +21,24 @@ export default function useInventoryStep(config, resource, visitedSteps, i18n) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function getStep(config, meta, i18n, visitedSteps) {
|
||||||
function getStep(config, hasErrors, i18n) {
|
|
||||||
if (!config.ask_inventory_on_launch) {
|
if (!config.ask_inventory_on_launch) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
name: <StepName hasErrors={hasErrors}>{i18n._(t`Inventory`)}</StepName>,
|
key: 3,
|
||||||
|
name: (
|
||||||
|
<StepName
|
||||||
|
hasErrors={
|
||||||
|
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
|
(!meta.value || meta.error)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{i18n._(t`Inventory`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
component: <InventoryStep i18n={i18n} />,
|
component: <InventoryStep i18n={i18n} />,
|
||||||
};
|
enableNext: true,
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialValues(config, resource) {
|
|
||||||
if (!config.ask_inventory_on_launch) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
inventory: resource?.summary_fields?.inventory || null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import OtherPromptsStep from './OtherPromptsStep';
|
import OtherPromptsStep from './OtherPromptsStep';
|
||||||
import StepName from './StepName';
|
|
||||||
|
|
||||||
const STEP_ID = 'other';
|
const STEP_ID = 'other';
|
||||||
|
|
||||||
export default function useOtherPrompt(config, resource, visitedSteps, i18n) {
|
export default function useOtherPrompt(config, i18n) {
|
||||||
const [stepErrors, setStepErrors] = useState({});
|
|
||||||
|
|
||||||
const validate = values => {
|
|
||||||
const errors = {};
|
|
||||||
if (config.ask_job_type_on_launch && !values.job_type) {
|
|
||||||
errors.job_type = i18n._(t`This field must not be blank`);
|
|
||||||
}
|
|
||||||
setStepErrors(errors);
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(config, hasErrors, i18n),
|
step: getStep(config, i18n),
|
||||||
initialValues: getInitialValues(config, resource),
|
|
||||||
validate,
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: stepErrors,
|
formError: null,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldsTouched => {
|
||||||
setFieldsTouched({
|
setFieldsTouched({
|
||||||
job_type: true,
|
job_type: true,
|
||||||
@@ -40,13 +24,13 @@ export default function useOtherPrompt(config, resource, visitedSteps, i18n) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStep(config, hasErrors, i18n) {
|
function getStep(config, i18n) {
|
||||||
if (!shouldShowPrompt(config)) {
|
if (!shouldShowPrompt(config)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
name: <StepName hasErrors={hasErrors}>{i18n._(t`Other Prompts`)}</StepName>,
|
name: i18n._(t`Other Prompts`),
|
||||||
component: <OtherPromptsStep config={config} i18n={i18n} />,
|
component: <OtherPromptsStep config={config} i18n={i18n} />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -63,32 +47,3 @@ function shouldShowPrompt(config) {
|
|||||||
config.ask_diff_mode_on_launch
|
config.ask_diff_mode_on_launch
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialValues(config, resource) {
|
|
||||||
const initialValues = {};
|
|
||||||
if (config.ask_job_type_on_launch) {
|
|
||||||
initialValues.job_type = resource.job_type || '';
|
|
||||||
}
|
|
||||||
if (config.ask_limit_on_launch) {
|
|
||||||
initialValues.limit = resource.limit || '';
|
|
||||||
}
|
|
||||||
if (config.ask_verbosity_on_launch) {
|
|
||||||
initialValues.verbosity = resource.verbosity || 0;
|
|
||||||
}
|
|
||||||
if (config.ask_tags_on_launch) {
|
|
||||||
initialValues.job_tags = resource.job_tags || '';
|
|
||||||
}
|
|
||||||
if (config.ask_skip_tags_on_launch) {
|
|
||||||
initialValues.skip_tags = resource.skip_tags || '';
|
|
||||||
}
|
|
||||||
if (config.ask_variables_on_launch) {
|
|
||||||
initialValues.extra_vars = resource.extra_vars || '---';
|
|
||||||
}
|
|
||||||
if (config.ask_scm_branch_on_launch) {
|
|
||||||
initialValues.scm_branch = resource.scm_branch || '';
|
|
||||||
}
|
|
||||||
if (config.ask_diff_mode_on_launch) {
|
|
||||||
initialValues.diff_mode = resource.diff_mode || false;
|
|
||||||
}
|
|
||||||
return initialValues;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import PreviewStep from './PreviewStep';
|
import PreviewStep from './PreviewStep';
|
||||||
|
|
||||||
@@ -8,9 +9,29 @@ export default function usePreviewStep(
|
|||||||
config,
|
config,
|
||||||
resource,
|
resource,
|
||||||
survey,
|
survey,
|
||||||
formErrors,
|
hasErrors,
|
||||||
i18n
|
i18n
|
||||||
) {
|
) {
|
||||||
|
const { values: formikValues, errors } = useFormikContext();
|
||||||
|
|
||||||
|
const formErrorsContent = [];
|
||||||
|
if (config.ask_inventory_on_launch && !formikValues.inventory) {
|
||||||
|
formErrorsContent.push({
|
||||||
|
inventory: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const hasSurveyError = Object.keys(errors).find(e => e.includes('survey'));
|
||||||
|
if (
|
||||||
|
config.survey_enabled &&
|
||||||
|
(config.variables_needed_to_start ||
|
||||||
|
config.variables_needed_to_start.length === 0) &&
|
||||||
|
hasSurveyError
|
||||||
|
) {
|
||||||
|
formErrorsContent.push({
|
||||||
|
survey: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: {
|
step: {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
@@ -20,14 +41,13 @@ export default function usePreviewStep(
|
|||||||
config={config}
|
config={config}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
survey={survey}
|
survey={survey}
|
||||||
formErrors={formErrors}
|
formErrors={hasErrors}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: Object.keys(formErrors).length === 0,
|
enableNext: !hasErrors,
|
||||||
nextButtonText: i18n._(t`Launch`),
|
nextButtonText: i18n._(t`Launch`),
|
||||||
},
|
},
|
||||||
initialValues: {},
|
initialValues: {},
|
||||||
validate: () => ({}),
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
error: null,
|
error: null,
|
||||||
setTouched: () => {},
|
setTouched: () => {},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../api';
|
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../api';
|
||||||
import SurveyStep from './SurveyStep';
|
import SurveyStep from './SurveyStep';
|
||||||
@@ -7,27 +8,27 @@ import StepName from './StepName';
|
|||||||
|
|
||||||
const STEP_ID = 'survey';
|
const STEP_ID = 'survey';
|
||||||
|
|
||||||
export default function useSurveyStep(config, resource, visitedSteps, i18n) {
|
export default function useSurveyStep(config, visitedSteps, i18n) {
|
||||||
const [stepErrors, setStepErrors] = useState({});
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
|
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
if (!config.survey_enabled) {
|
if (!config.survey_enabled) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const { data } =
|
const { data } = config?.workflow_job_template_data
|
||||||
resource.type === 'workflow_job_template'
|
? await WorkflowJobTemplatesAPI.readSurvey(
|
||||||
? await WorkflowJobTemplatesAPI.readSurvey(resource.id)
|
config?.workflow_job_template_data?.id
|
||||||
: await JobTemplatesAPI.readSurvey(resource.id);
|
)
|
||||||
|
: await JobTemplatesAPI.readSurvey(config?.job_template_data?.id);
|
||||||
return data;
|
return data;
|
||||||
}, [config.survey_enabled, resource])
|
}, [config])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSurvey();
|
fetchSurvey();
|
||||||
}, [fetchSurvey]);
|
}, [fetchSurvey]);
|
||||||
|
|
||||||
const validate = values => {
|
const validate = () => {
|
||||||
if (!config.survey_enabled || !survey || !survey.spec) {
|
if (!config.survey_enabled || !survey || !survey.spec) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -42,20 +43,16 @@ export default function useSurveyStep(config, resource, visitedSteps, i18n) {
|
|||||||
errors[`survey_${question.variable}`] = errMessage;
|
errors[`survey_${question.variable}`] = errMessage;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setStepErrors(errors);
|
|
||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
const formError = Object.keys(validate()).length > 0;
|
||||||
const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(config, survey, hasErrors, i18n),
|
step: getStep(config, survey, formError, i18n, visitedSteps),
|
||||||
|
formError,
|
||||||
initialValues: getInitialValues(config, survey),
|
initialValues: getInitialValues(config, survey),
|
||||||
validate,
|
|
||||||
survey,
|
survey,
|
||||||
isReady: !isLoading && !!survey,
|
isReady: !isLoading && !!survey,
|
||||||
contentError: error,
|
contentError: error,
|
||||||
formError: stepErrors,
|
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldsTouched => {
|
||||||
if (!survey || !survey.spec) {
|
if (!survey || !survey.spec) {
|
||||||
return;
|
return;
|
||||||
@@ -87,34 +84,49 @@ function validateField(question, value, i18n) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (question.required && !value && value !== 0) {
|
if (
|
||||||
|
question.required &&
|
||||||
|
((!value && value !== 0) || (Array.isArray(value) && value.length === 0))
|
||||||
|
) {
|
||||||
return i18n._(t`This field must not be blank`);
|
return i18n._(t`This field must not be blank`);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function getStep(config, survey, hasErrors, i18n, visitedSteps) {
|
||||||
function getStep(config, survey, hasErrors, i18n) {
|
|
||||||
if (!config.survey_enabled) {
|
if (!config.survey_enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
name: <StepName hasErrors={hasErrors}>{i18n._(t`Survey`)}</StepName>,
|
key: 6,
|
||||||
|
name: (
|
||||||
|
<StepName
|
||||||
|
hasErrors={Object.keys(visitedSteps).includes(STEP_ID) && hasErrors}
|
||||||
|
>
|
||||||
|
{i18n._(t`Survey`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
component: <SurveyStep survey={survey} i18n={i18n} />,
|
component: <SurveyStep survey={survey} i18n={i18n} />,
|
||||||
|
enableNext: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialValues(config, survey) {
|
function getInitialValues(config, survey) {
|
||||||
if (!config.survey_enabled || !survey) {
|
if (!config.survey_enabled || !survey) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const values = {};
|
const surveyValues = {};
|
||||||
survey.spec.forEach(question => {
|
survey.spec.forEach(question => {
|
||||||
if (question.type === 'multiselect') {
|
if (question.type === 'multiselect') {
|
||||||
values[`survey_${question.variable}`] = question.default.split('\n');
|
if (question.default === '') {
|
||||||
|
surveyValues[`survey_${question.variable}`] = [];
|
||||||
|
} else {
|
||||||
|
surveyValues[`survey_${question.variable}`] = question.default.split(
|
||||||
|
'\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
values[`survey_${question.variable}`] = question.default;
|
surveyValues[`survey_${question.variable}`] = question.default;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return values;
|
return surveyValues;
|
||||||
}
|
}
|
||||||
|
|||||||
69
awx/ui_next/src/components/LaunchPrompt/useLaunchSteps.js
Normal file
69
awx/ui_next/src/components/LaunchPrompt/useLaunchSteps.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import useInventoryStep from './steps/useInventoryStep';
|
||||||
|
import useCredentialsStep from './steps/useCredentialsStep';
|
||||||
|
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
||||||
|
import useSurveyStep from './steps/useSurveyStep';
|
||||||
|
import usePreviewStep from './steps/usePreviewStep';
|
||||||
|
|
||||||
|
export default function useLaunchSteps(config, resource, i18n) {
|
||||||
|
const [visited, setVisited] = useState({});
|
||||||
|
const steps = [
|
||||||
|
useInventoryStep(config, visited, i18n),
|
||||||
|
useCredentialsStep(config, i18n),
|
||||||
|
useOtherPromptsStep(config, i18n),
|
||||||
|
useSurveyStep(config, visited, i18n),
|
||||||
|
];
|
||||||
|
const { resetForm, values: formikValues } = useFormikContext();
|
||||||
|
const hasErrors = steps.some(step => step.formError);
|
||||||
|
|
||||||
|
const surveyStepIndex = steps.findIndex(step => step.survey);
|
||||||
|
steps.push(
|
||||||
|
usePreviewStep(
|
||||||
|
config,
|
||||||
|
resource,
|
||||||
|
steps[surveyStepIndex]?.survey,
|
||||||
|
hasErrors,
|
||||||
|
i18n
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
||||||
|
const isReady = !steps.some(s => !s.isReady);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (surveyStepIndex > -1 && isReady) {
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
...formikValues,
|
||||||
|
...steps[surveyStepIndex].initialValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isReady]);
|
||||||
|
|
||||||
|
const stepWithError = steps.find(s => s.contentError);
|
||||||
|
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
steps: pfSteps,
|
||||||
|
isReady,
|
||||||
|
visitStep: stepId =>
|
||||||
|
setVisited({
|
||||||
|
...visited,
|
||||||
|
[stepId]: true,
|
||||||
|
}),
|
||||||
|
visitAllSteps: setFieldsTouched => {
|
||||||
|
setVisited({
|
||||||
|
inventory: true,
|
||||||
|
credentials: true,
|
||||||
|
other: true,
|
||||||
|
survey: true,
|
||||||
|
preview: true,
|
||||||
|
});
|
||||||
|
steps.forEach(s => s.setTouched(setFieldsTouched));
|
||||||
|
},
|
||||||
|
contentError,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import useInventoryStep from './steps/useInventoryStep';
|
|
||||||
import useCredentialsStep from './steps/useCredentialsStep';
|
|
||||||
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
|
||||||
import useSurveyStep from './steps/useSurveyStep';
|
|
||||||
import usePreviewStep from './steps/usePreviewStep';
|
|
||||||
|
|
||||||
export default function useSteps(config, resource, i18n) {
|
|
||||||
const [visited, setVisited] = useState({});
|
|
||||||
const steps = [
|
|
||||||
useInventoryStep(config, resource, visited, i18n),
|
|
||||||
useCredentialsStep(config, resource, visited, i18n),
|
|
||||||
useOtherPromptsStep(config, resource, visited, i18n),
|
|
||||||
useSurveyStep(config, resource, visited, i18n),
|
|
||||||
];
|
|
||||||
|
|
||||||
const formErrorsContent = steps
|
|
||||||
.filter(s => s?.formError && Object.keys(s.formError).length > 0)
|
|
||||||
.map(({ formError }) => formError);
|
|
||||||
|
|
||||||
steps.push(
|
|
||||||
usePreviewStep(config, resource, steps[3].survey, formErrorsContent, i18n)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
|
||||||
const initialValues = steps.reduce((acc, cur) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
...cur.initialValues,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
const isReady = !steps.some(s => !s.isReady);
|
|
||||||
|
|
||||||
const stepWithError = steps.find(s => s.contentError);
|
|
||||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
|
||||||
|
|
||||||
const validate = values => {
|
|
||||||
const errors = steps.reduce((acc, cur) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
...cur.validate(values),
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
if (Object.keys(errors).length) {
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
steps: pfSteps,
|
|
||||||
initialValues,
|
|
||||||
isReady,
|
|
||||||
validate,
|
|
||||||
visitStep: stepId => setVisited({ ...visited, [stepId]: true }),
|
|
||||||
visitAllSteps: setFieldsTouched => {
|
|
||||||
setVisited({
|
|
||||||
inventory: true,
|
|
||||||
credentials: true,
|
|
||||||
other: true,
|
|
||||||
survey: true,
|
|
||||||
preview: true,
|
|
||||||
});
|
|
||||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
|
||||||
},
|
|
||||||
contentError,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user