mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 23:46:05 -03:30
Merge pull request #6955 from keithjgrant/5909-jt-launch-prompt-4
JT Launch prompt preview & validation Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
export default function getErrorMessage(response) {
|
export default function getErrorMessage(response) {
|
||||||
if (!response.data) {
|
if (!response?.data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof response.data === 'string') {
|
if (typeof response.data === 'string') {
|
||||||
|
|||||||
@@ -3,84 +3,29 @@ 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 } from 'formik';
|
||||||
import InventoryStep from './InventoryStep';
|
import ContentError from '@components/ContentError';
|
||||||
import CredentialsStep from './CredentialsStep';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import OtherPromptsStep from './OtherPromptsStep';
|
|
||||||
import SurveyStep from './SurveyStep';
|
|
||||||
import PreviewStep from './PreviewStep';
|
|
||||||
import mergeExtraVars from './mergeExtraVars';
|
import mergeExtraVars from './mergeExtraVars';
|
||||||
|
import useSteps from './useSteps';
|
||||||
|
import getSurveyValues from './getSurveyValues';
|
||||||
|
|
||||||
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||||
const steps = [];
|
const {
|
||||||
const initialValues = {};
|
steps,
|
||||||
if (config.ask_inventory_on_launch) {
|
initialValues,
|
||||||
initialValues.inventory = resource?.summary_fields?.inventory || null;
|
isReady,
|
||||||
steps.push({
|
validate,
|
||||||
name: i18n._(t`Inventory`),
|
visitStep,
|
||||||
component: <InventoryStep />,
|
visitAllSteps,
|
||||||
});
|
contentError,
|
||||||
}
|
} = useSteps(config, resource, i18n);
|
||||||
if (config.ask_credential_on_launch) {
|
|
||||||
initialValues.credentials = resource?.summary_fields?.credentials || [];
|
|
||||||
steps.push({
|
|
||||||
name: i18n._(t`Credentials`),
|
|
||||||
component: <CredentialsStep />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add Credential Passwords step
|
if (contentError) {
|
||||||
|
return <ContentError error={contentError} />;
|
||||||
if (config.ask_job_type_on_launch) {
|
|
||||||
initialValues.job_type = resource.job_type || '';
|
|
||||||
}
|
}
|
||||||
if (config.ask_limit_on_launch) {
|
if (!isReady) {
|
||||||
initialValues.limit = resource.limit || '';
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
config.ask_job_type_on_launch ||
|
|
||||||
config.ask_limit_on_launch ||
|
|
||||||
config.ask_verbosity_on_launch ||
|
|
||||||
config.ask_tags_on_launch ||
|
|
||||||
config.ask_skip_tags_on_launch ||
|
|
||||||
config.ask_variables_on_launch ||
|
|
||||||
config.ask_scm_branch_on_launch ||
|
|
||||||
config.ask_diff_mode_on_launch
|
|
||||||
) {
|
|
||||||
steps.push({
|
|
||||||
name: i18n._(t`Other Prompts`),
|
|
||||||
component: <OtherPromptsStep config={config} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (config.survey_enabled) {
|
|
||||||
initialValues.survey = {};
|
|
||||||
steps.push({
|
|
||||||
name: i18n._(t`Survey`),
|
|
||||||
component: <SurveyStep template={resource} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
steps.push({
|
|
||||||
name: i18n._(t`Preview`),
|
|
||||||
component: <PreviewStep />,
|
|
||||||
nextButtonText: i18n._(t`Launch`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const submit = values => {
|
const submit = values => {
|
||||||
const postValues = {};
|
const postValues = {};
|
||||||
@@ -89,23 +34,40 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
|||||||
postValues[key] = value;
|
postValues[key] = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const surveyValues = getSurveyValues(values);
|
||||||
setValue('inventory_id', values.inventory?.id);
|
setValue('inventory_id', values.inventory?.id);
|
||||||
setValue('credentials', values.credentials?.map(c => c.id));
|
setValue('credentials', values.credentials?.map(c => c.id));
|
||||||
setValue('job_type', values.job_type);
|
setValue('job_type', values.job_type);
|
||||||
setValue('limit', values.limit);
|
setValue('limit', values.limit);
|
||||||
setValue('job_tags', values.job_tags);
|
setValue('job_tags', values.job_tags);
|
||||||
setValue('skip_tags', values.skip_tags);
|
setValue('skip_tags', values.skip_tags);
|
||||||
setValue('extra_vars', mergeExtraVars(values.extra_vars, values.survey));
|
setValue('extra_vars', mergeExtraVars(values.extra_vars, surveyValues));
|
||||||
onLaunch(postValues);
|
onLaunch(postValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={initialValues} onSubmit={submit}>
|
<Formik initialValues={initialValues} onSubmit={submit} validate={validate}>
|
||||||
{({ handleSubmit }) => (
|
{({ validateForm, setTouched, handleSubmit }) => (
|
||||||
<Wizard
|
<Wizard
|
||||||
isOpen
|
isOpen
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
onSave={handleSubmit}
|
onSave={handleSubmit}
|
||||||
|
onNext={async (nextStep, prevStep) => {
|
||||||
|
if (nextStep.id === 'preview') {
|
||||||
|
visitAllSteps(setTouched);
|
||||||
|
} else {
|
||||||
|
visitStep(prevStep.prevId);
|
||||||
|
}
|
||||||
|
await validateForm();
|
||||||
|
}}
|
||||||
|
onGoToStep={async (newStep, prevStep) => {
|
||||||
|
if (newStep.id === 'preview') {
|
||||||
|
visitAllSteps(setTouched);
|
||||||
|
} else {
|
||||||
|
visitStep(prevStep.prevId);
|
||||||
|
}
|
||||||
|
await validateForm();
|
||||||
|
}}
|
||||||
title={i18n._(t`Prompts`)}
|
title={i18n._(t`Prompts`)}
|
||||||
steps={steps}
|
steps={steps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act, isElementOfType } from 'react-dom/test-utils';
|
import { act, isElementOfType } from 'react-dom/test-utils';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
import LaunchPrompt from './LaunchPrompt';
|
import LaunchPrompt from './LaunchPrompt';
|
||||||
import InventoryStep from './InventoryStep';
|
import InventoryStep from './steps/InventoryStep';
|
||||||
import CredentialsStep from './CredentialsStep';
|
import CredentialsStep from './steps/CredentialsStep';
|
||||||
import OtherPromptsStep from './OtherPromptsStep';
|
import OtherPromptsStep from './steps/OtherPromptsStep';
|
||||||
import PreviewStep from './PreviewStep';
|
import PreviewStep from './steps/PreviewStep';
|
||||||
import { InventoriesAPI, CredentialsAPI, CredentialTypesAPI } from '@api';
|
import {
|
||||||
|
InventoriesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
} from '@api';
|
||||||
|
|
||||||
jest.mock('@api/models/Inventories');
|
jest.mock('@api/models/Inventories');
|
||||||
jest.mock('@api/models/CredentialTypes');
|
jest.mock('@api/models/CredentialTypes');
|
||||||
jest.mock('@api/models/Credentials');
|
jest.mock('@api/models/Credentials');
|
||||||
|
jest.mock('@api/models/JobTemplates');
|
||||||
|
|
||||||
let config;
|
let config;
|
||||||
const resource = {
|
const resource = {
|
||||||
@@ -31,6 +37,13 @@ describe('LaunchPrompt', () => {
|
|||||||
data: { results: [{ id: 1 }], count: 1 },
|
data: { results: [{ id: 1 }], count: 1 },
|
||||||
});
|
});
|
||||||
CredentialTypesAPI.loadAllTypes({ data: { results: [{ type: 'ssh' }] } });
|
CredentialTypesAPI.loadAllTypes({ data: { results: [{ type: 'ssh' }] } });
|
||||||
|
JobTemplatesAPI.readSurvey.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
spec: [{ type: 'text', variable: 'foo' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
can_start_without_user_input: false,
|
can_start_without_user_input: false,
|
||||||
@@ -73,13 +86,14 @@ describe('LaunchPrompt', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const steps = wrapper.find('Wizard').prop('steps');
|
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||||
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(5);
|
expect(steps).toHaveLength(5);
|
||||||
expect(steps[0].name).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).toEqual('Other Prompts');
|
expect(steps[2].name.props.children).toEqual('Other Prompts');
|
||||||
expect(steps[3].name).toEqual('Survey');
|
expect(steps[3].name.props.children).toEqual('Survey');
|
||||||
expect(steps[4].name).toEqual('Preview');
|
expect(steps[4].name).toEqual('Preview');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,10 +112,11 @@ describe('LaunchPrompt', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const steps = wrapper.find('Wizard').prop('steps');
|
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||||
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(2);
|
expect(steps).toHaveLength(2);
|
||||||
expect(steps[0].name).toEqual('Inventory');
|
expect(steps[0].name.props.children).toEqual('Inventory');
|
||||||
expect(isElementOfType(steps[0].component, InventoryStep)).toEqual(true);
|
expect(isElementOfType(steps[0].component, InventoryStep)).toEqual(true);
|
||||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||||
});
|
});
|
||||||
@@ -121,7 +136,8 @@ describe('LaunchPrompt', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const steps = wrapper.find('Wizard').prop('steps');
|
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||||
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(2);
|
expect(steps).toHaveLength(2);
|
||||||
expect(steps[0].name).toEqual('Credentials');
|
expect(steps[0].name).toEqual('Credentials');
|
||||||
@@ -144,10 +160,11 @@ describe('LaunchPrompt', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const steps = wrapper.find('Wizard').prop('steps');
|
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||||
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(2);
|
expect(steps).toHaveLength(2);
|
||||||
expect(steps[0].name).toEqual('Other Prompts');
|
expect(steps[0].name.props.children).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 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
function PreviewStep() {
|
|
||||||
return <div>Preview of selected values will appear here</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PreviewStep;
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export default function getSurveyValues(values) {
|
||||||
|
const surveyValues = {};
|
||||||
|
Object.keys(values).forEach(key => {
|
||||||
|
if (key.startsWith('survey_')) {
|
||||||
|
surveyValues[key.substr(7)] = values[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return surveyValues;
|
||||||
|
}
|
||||||
@@ -8,4 +8,12 @@ export default function mergeExtraVars(extraVars, survey = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: "safe" version that obscures passwords for preview step
|
export function maskPasswords(vars, passwordKeys) {
|
||||||
|
const updated = { ...vars };
|
||||||
|
passwordKeys.forEach(key => {
|
||||||
|
if (typeof updated[key] !== 'undefined') {
|
||||||
|
updated[key] = '········';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import mergeExtraVars from './mergeExtraVars';
|
import mergeExtraVars, { maskPasswords } from './mergeExtraVars';
|
||||||
|
|
||||||
describe('mergeExtraVars', () => {
|
describe('mergeExtraVars', () => {
|
||||||
test('should handle yaml string', () => {
|
test('should handle yaml string', () => {
|
||||||
@@ -31,4 +31,32 @@ describe('mergeExtraVars', () => {
|
|||||||
bar: 'baz',
|
bar: 'baz',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('maskPasswords', () => {
|
||||||
|
test('should mask password fields', () => {
|
||||||
|
const vars = {
|
||||||
|
one: 'alpha',
|
||||||
|
two: 'bravo',
|
||||||
|
three: 'charlie',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(maskPasswords(vars, ['one', 'three'])).toEqual({
|
||||||
|
one: '········',
|
||||||
|
two: 'bravo',
|
||||||
|
three: '········',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should mask empty strings', () => {
|
||||||
|
const vars = {
|
||||||
|
one: '',
|
||||||
|
two: 'bravo',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(maskPasswords(vars, ['one', 'three'])).toEqual({
|
||||||
|
one: '········',
|
||||||
|
two: 'bravo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,15 +12,19 @@ import CredentialChip from '@components/CredentialChip';
|
|||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
import useRequest from '@util/useRequest';
|
import useRequest from '@util/useRequest';
|
||||||
|
import { required } from '@util/validators';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('inventory', {
|
const QS_CONFIG = getQSConfig('credential', {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 5,
|
page_size: 5,
|
||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function CredentialsStep({ i18n }) {
|
function CredentialsStep({ i18n }) {
|
||||||
const [field, , helpers] = useField('credentials');
|
const [field, , helpers] = useField({
|
||||||
|
name: 'credentials',
|
||||||
|
validate: required(null, i18n),
|
||||||
|
});
|
||||||
const [selectedType, setSelectedType] = useState(null);
|
const [selectedType, setSelectedType] = useState(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import useRequest from '@util/useRequest';
|
|||||||
import OptionsList from '@components/OptionsList';
|
import OptionsList from '@components/OptionsList';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
|
import { required } from '@util/validators';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('inventory', {
|
const QS_CONFIG = getQSConfig('inventory', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -17,7 +18,10 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryStep({ i18n }) {
|
function InventoryStep({ i18n }) {
|
||||||
const [field, , helpers] = useField('inventory');
|
const [field, , helpers] = useField({
|
||||||
|
name: 'inventory',
|
||||||
|
validate: required(null, i18n),
|
||||||
|
});
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import PromptDetail from '@components/PromptDetail';
|
||||||
|
import mergeExtraVars, { maskPasswords } from '../mergeExtraVars';
|
||||||
|
import getSurveyValues from '../getSurveyValues';
|
||||||
|
|
||||||
|
function PreviewStep({ resource, config, survey, formErrors }) {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
const surveyValues = getSurveyValues(values);
|
||||||
|
const passwordFields = survey.spec
|
||||||
|
.filter(q => q.type === 'password')
|
||||||
|
.map(q => q.variable);
|
||||||
|
const masked = maskPasswords(surveyValues, passwordFields);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PromptDetail
|
||||||
|
resource={resource}
|
||||||
|
launchConfig={config}
|
||||||
|
overrides={{
|
||||||
|
...values,
|
||||||
|
extra_vars: yaml.safeDump(mergeExtraVars(values.extra_vars, masked)),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{formErrors && (
|
||||||
|
<ul css="color: red">
|
||||||
|
{Object.keys(formErrors).map(
|
||||||
|
field => `${field}: ${formErrors[field]}`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewStep;
|
||||||
37
awx/ui_next/src/components/LaunchPrompt/steps/StepName.jsx
Normal file
37
awx/ui_next/src/components/LaunchPrompt/steps/StepName.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Tooltip } from '@patternfly/react-core';
|
||||||
|
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
const AlertText = styled.div`
|
||||||
|
color: var(--pf-global--danger-color--200);
|
||||||
|
font-weight: var(--pf-global--FontWeight--bold);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function StepName({ hasErrors, children, i18n }) {
|
||||||
|
if (!hasErrors) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertText>
|
||||||
|
{children}
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(t`This step contains errors`)}
|
||||||
|
trigger="click mouseenter focus"
|
||||||
|
>
|
||||||
|
<ExclamationCircleIcon css="color: var(--pf-global--danger-color--100)" />
|
||||||
|
</Tooltip>
|
||||||
|
</AlertText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(StepName);
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Formik, useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -11,9 +10,6 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import FormField, { FieldTooltip } from '@components/FormField';
|
import FormField, { FieldTooltip } from '@components/FormField';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
|
||||||
import ContentError from '@components/ContentError';
|
|
||||||
import useRequest from '@util/useRequest';
|
|
||||||
import {
|
import {
|
||||||
required,
|
required,
|
||||||
minMaxValue,
|
minMaxValue,
|
||||||
@@ -22,54 +18,9 @@ import {
|
|||||||
integer,
|
integer,
|
||||||
combine,
|
combine,
|
||||||
} from '@util/validators';
|
} from '@util/validators';
|
||||||
|
import { Survey } from '@types';
|
||||||
|
|
||||||
function SurveyStep({ template, i18n }) {
|
function SurveyStep({ survey, i18n }) {
|
||||||
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
|
|
||||||
useCallback(async () => {
|
|
||||||
const { data } =
|
|
||||||
template.type === 'workflow_job_template'
|
|
||||||
? await WorkflowJobTemplatesAPI.readSurvey(template.id)
|
|
||||||
: await JobTemplatesAPI.readSurvey(template.id);
|
|
||||||
return data;
|
|
||||||
}, [template])
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSurvey();
|
|
||||||
}, [fetchSurvey]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ContentError error={error} />;
|
|
||||||
}
|
|
||||||
if (isLoading || !survey) {
|
|
||||||
return <ContentLoading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialValues = {};
|
|
||||||
survey.spec.forEach(question => {
|
|
||||||
if (question.type === 'multiselect') {
|
|
||||||
initialValues[question.variable] = question.default.split('\n');
|
|
||||||
} else {
|
|
||||||
initialValues[question.variable] = question.default;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SurveySubForm survey={survey} initialValues={initialValues} i18n={i18n} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a nested Formik form to perform validation on individual
|
|
||||||
// survey questions. When changes to the inner form occur (onBlur), the
|
|
||||||
// values for all questions are added to the outer form's `survey` field
|
|
||||||
// as a single object.
|
|
||||||
function SurveySubForm({ survey, initialValues, i18n }) {
|
|
||||||
const [, , surveyFieldHelpers] = useField('survey');
|
|
||||||
useEffect(() => {
|
|
||||||
// set survey initial values to parent form
|
|
||||||
surveyFieldHelpers.setValue(initialValues);
|
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fieldTypes = {
|
const fieldTypes = {
|
||||||
text: TextField,
|
text: TextField,
|
||||||
textarea: TextField,
|
textarea: TextField,
|
||||||
@@ -80,21 +31,19 @@ function SurveySubForm({ survey, initialValues, i18n }) {
|
|||||||
float: NumberField,
|
float: NumberField,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={initialValues}>
|
<Form>
|
||||||
{({ values }) => (
|
{survey.spec.map(question => {
|
||||||
<Form onBlur={() => surveyFieldHelpers.setValue(values)}>
|
const Field = fieldTypes[question.type];
|
||||||
{' '}
|
return (
|
||||||
{survey.spec.map(question => {
|
<Field key={question.variable} question={question} i18n={i18n} />
|
||||||
const Field = fieldTypes[question.type];
|
);
|
||||||
return (
|
})}
|
||||||
<Field key={question.variable} question={question} i18n={i18n} />
|
</Form>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
SurveyStep.propTypes = {
|
||||||
|
survey: Survey.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
function TextField({ question, i18n }) {
|
function TextField({ question, i18n }) {
|
||||||
const validators = [
|
const validators = [
|
||||||
@@ -105,7 +54,7 @@ function TextField({ question, i18n }) {
|
|||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
id={`survey-question-${question.variable}`}
|
id={`survey-question-${question.variable}`}
|
||||||
name={question.variable}
|
name={`survey_${question.variable}`}
|
||||||
label={question.question_name}
|
label={question.question_name}
|
||||||
tooltip={question.question_description}
|
tooltip={question.question_description}
|
||||||
isRequired={question.required}
|
isRequired={question.required}
|
||||||
@@ -126,7 +75,7 @@ function NumberField({ question, i18n }) {
|
|||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
id={`survey-question-${question.variable}`}
|
id={`survey-question-${question.variable}`}
|
||||||
name={question.variable}
|
name={`survey_${question.variable}`}
|
||||||
label={question.question_name}
|
label={question.question_name}
|
||||||
tooltip={question.question_description}
|
tooltip={question.question_description}
|
||||||
isRequired={question.required}
|
isRequired={question.required}
|
||||||
@@ -139,7 +88,7 @@ function NumberField({ question, i18n }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MultipleChoiceField({ question }) {
|
function MultipleChoiceField({ question }) {
|
||||||
const [field, meta] = useField(question.variable);
|
const [field, meta] = useField(`survey_${question.variable}`);
|
||||||
const id = `survey-question-${question.variable}`;
|
const id = `survey-question-${question.variable}`;
|
||||||
const isValid = !(meta.touched && meta.error);
|
const isValid = !(meta.touched && meta.error);
|
||||||
return (
|
return (
|
||||||
@@ -167,7 +116,7 @@ function MultipleChoiceField({ question }) {
|
|||||||
|
|
||||||
function MultiSelectField({ question }) {
|
function MultiSelectField({ question }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [field, meta, helpers] = useField(question.variable);
|
const [field, meta, helpers] = useField(`survey_${question.variable}`);
|
||||||
const id = `survey-question-${question.variable}`;
|
const id = `survey-question-${question.variable}`;
|
||||||
const isValid = !(meta.touched && meta.error);
|
const isValid = !(meta.touched && meta.error);
|
||||||
return (
|
return (
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import CredentialsStep from './CredentialsStep';
|
||||||
|
|
||||||
|
const STEP_ID = 'credentials';
|
||||||
|
|
||||||
|
export default function useCredentialsStep(
|
||||||
|
config,
|
||||||
|
resource,
|
||||||
|
visitedSteps,
|
||||||
|
i18n
|
||||||
|
) {
|
||||||
|
const validate = () => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: getStep(config, i18n),
|
||||||
|
initialValues: getInitialValues(config, resource),
|
||||||
|
validate,
|
||||||
|
isReady: true,
|
||||||
|
error: null,
|
||||||
|
setTouched: setFieldsTouched => {
|
||||||
|
setFieldsTouched({
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep(config, i18n) {
|
||||||
|
if (!config.ask_credential_on_launch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: i18n._(t`Credentials`),
|
||||||
|
component: <CredentialsStep i18n={i18n} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValues(config, resource) {
|
||||||
|
if (!config.ask_credential_on_launch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
credentials: resource?.summary_fields?.credentials || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import InventoryStep from './InventoryStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'inventory';
|
||||||
|
|
||||||
|
export default function useInventoryStep(config, resource, visitedSteps, i18n) {
|
||||||
|
const [stepErrors, setStepErrors] = useState({});
|
||||||
|
|
||||||
|
const validate = values => {
|
||||||
|
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 {
|
||||||
|
step: getStep(config, hasErrors, i18n),
|
||||||
|
initialValues: getInitialValues(config, resource),
|
||||||
|
validate,
|
||||||
|
isReady: true,
|
||||||
|
error: null,
|
||||||
|
setTouched: setFieldsTouched => {
|
||||||
|
setFieldsTouched({
|
||||||
|
inventory: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep(config, hasErrors, i18n) {
|
||||||
|
if (!config.ask_inventory_on_launch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: <StepName hasErrors={hasErrors}>{i18n._(t`Inventory`)}</StepName>,
|
||||||
|
component: <InventoryStep i18n={i18n} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValues(config, resource) {
|
||||||
|
if (!config.ask_inventory_on_launch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
inventory: resource?.summary_fields?.inventory || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import OtherPromptsStep from './OtherPromptsStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'other';
|
||||||
|
|
||||||
|
export default function useOtherPrompt(config, resource, visitedSteps, 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 {
|
||||||
|
step: getStep(config, hasErrors, i18n),
|
||||||
|
initialValues: getInitialValues(config, resource),
|
||||||
|
validate,
|
||||||
|
isReady: true,
|
||||||
|
error: null,
|
||||||
|
setTouched: setFieldsTouched => {
|
||||||
|
setFieldsTouched({
|
||||||
|
job_type: true,
|
||||||
|
limit: true,
|
||||||
|
verbosity: true,
|
||||||
|
diff_mode: true,
|
||||||
|
job_tags: true,
|
||||||
|
skip_tags: true,
|
||||||
|
extra_vars: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep(config, hasErrors, i18n) {
|
||||||
|
if (!shouldShowPrompt(config)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: <StepName hasErrors={hasErrors}>{i18n._(t`Other Prompts`)}</StepName>,
|
||||||
|
component: <OtherPromptsStep config={config} i18n={i18n} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowPrompt(config) {
|
||||||
|
return (
|
||||||
|
config.ask_job_type_on_launch ||
|
||||||
|
config.ask_limit_on_launch ||
|
||||||
|
config.ask_verbosity_on_launch ||
|
||||||
|
config.ask_tags_on_launch ||
|
||||||
|
config.ask_skip_tags_on_launch ||
|
||||||
|
config.ask_variables_on_launch ||
|
||||||
|
config.ask_scm_branch_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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import PreviewStep from './PreviewStep';
|
||||||
|
|
||||||
|
const STEP_ID = 'preview';
|
||||||
|
|
||||||
|
export default function usePreviewStep(
|
||||||
|
config,
|
||||||
|
resource,
|
||||||
|
survey,
|
||||||
|
formErrors,
|
||||||
|
i18n
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
step: {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: i18n._(t`Preview`),
|
||||||
|
component: (
|
||||||
|
<PreviewStep
|
||||||
|
config={config}
|
||||||
|
resource={resource}
|
||||||
|
survey={survey}
|
||||||
|
formErrors={formErrors}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableNext: Object.keys(formErrors).length === 0,
|
||||||
|
nextButtonText: i18n._(t`Launch`),
|
||||||
|
},
|
||||||
|
initialValues: {},
|
||||||
|
validate: () => ({}),
|
||||||
|
isReady: true,
|
||||||
|
error: null,
|
||||||
|
setTouched: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
119
awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx
Normal file
119
awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import useRequest from '@util/useRequest';
|
||||||
|
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
|
||||||
|
import SurveyStep from './SurveyStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'survey';
|
||||||
|
|
||||||
|
export default function useSurveyStep(config, resource, visitedSteps, i18n) {
|
||||||
|
const [stepErrors, setStepErrors] = useState({});
|
||||||
|
|
||||||
|
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
if (!config.survey_enabled) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const { data } =
|
||||||
|
resource.type === 'workflow_job_template'
|
||||||
|
? await WorkflowJobTemplatesAPI.readSurvey(resource.id)
|
||||||
|
: await JobTemplatesAPI.readSurvey(resource.id);
|
||||||
|
return data;
|
||||||
|
}, [config.survey_enabled, resource])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSurvey();
|
||||||
|
}, [fetchSurvey]);
|
||||||
|
|
||||||
|
const validate = values => {
|
||||||
|
if (!config.survey_enabled || !survey || !survey.spec) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const errors = {};
|
||||||
|
survey.spec.forEach(question => {
|
||||||
|
const errMessage = validateField(
|
||||||
|
question,
|
||||||
|
values[`survey_${question.variable}`],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
if (errMessage) {
|
||||||
|
errors[`survey_${question.variable}`] = errMessage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setStepErrors(errors);
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: getStep(config, survey, hasErrors, i18n),
|
||||||
|
initialValues: getInitialValues(config, survey),
|
||||||
|
validate,
|
||||||
|
survey,
|
||||||
|
isReady: !isLoading && !!survey,
|
||||||
|
error,
|
||||||
|
setTouched: setFieldsTouched => {
|
||||||
|
if (!survey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fields = {};
|
||||||
|
survey.spec.forEach(question => {
|
||||||
|
fields[`survey_${question.variable}`] = true;
|
||||||
|
});
|
||||||
|
setFieldsTouched(fields);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateField(question, value, i18n) {
|
||||||
|
const isTextField = ['text', 'textarea'].includes(question.type);
|
||||||
|
const isNumeric = ['integer', 'float'].includes(question.type);
|
||||||
|
if (isTextField && (value || value === 0)) {
|
||||||
|
if (question.min && value.length < question.min) {
|
||||||
|
return i18n._(t`This field must be at least ${question.min} characters`);
|
||||||
|
}
|
||||||
|
if (question.max && value.length > question.max) {
|
||||||
|
return i18n._(t`This field must not exceed ${question.max} characters`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNumeric && (value || value === 0)) {
|
||||||
|
if (value < question.min || value > question.max) {
|
||||||
|
return i18n._(
|
||||||
|
t`This field must be a number and have a value between ${question.min} and ${question.max}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.required && !value && value !== 0) {
|
||||||
|
return i18n._(t`This field must not be blank`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep(config, survey, hasErrors, i18n) {
|
||||||
|
if (!config.survey_enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: <StepName hasErrors={hasErrors}>{i18n._(t`Survey`)}</StepName>,
|
||||||
|
component: <SurveyStep survey={survey} i18n={i18n} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValues(config, survey) {
|
||||||
|
if (!config.survey_enabled || !survey) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const values = {};
|
||||||
|
survey.spec.forEach(question => {
|
||||||
|
if (question.type === 'multiselect') {
|
||||||
|
values[`survey_${question.variable}`] = question.default.split('\n');
|
||||||
|
} else {
|
||||||
|
values[`survey_${question.variable}`] = question.default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
}
|
||||||
68
awx/ui_next/src/components/LaunchPrompt/useSteps.js
Normal file
68
awx/ui_next/src/components/LaunchPrompt/useSteps.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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),
|
||||||
|
];
|
||||||
|
steps.push(
|
||||||
|
usePreviewStep(
|
||||||
|
config,
|
||||||
|
resource,
|
||||||
|
steps[3].survey,
|
||||||
|
{}, // TODO: formErrors ?
|
||||||
|
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.error);
|
||||||
|
const contentError = stepWithError ? stepWithError.error : 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -77,81 +77,7 @@ function omitOverrides(resource, overrides) {
|
|||||||
return clonedResource;
|
return clonedResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: When prompting is hooked up, update function
|
function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||||
// to filter based on prompt overrides
|
|
||||||
function partitionPromptDetails(resource, launchConfig) {
|
|
||||||
const { defaults = {} } = launchConfig;
|
|
||||||
const overrides = {};
|
|
||||||
|
|
||||||
if (launchConfig.ask_credential_on_launch) {
|
|
||||||
let isEqual;
|
|
||||||
const defaultCreds = defaults.credentials;
|
|
||||||
const currentCreds = resource?.summary_fields?.credentials;
|
|
||||||
|
|
||||||
if (defaultCreds?.length === currentCreds?.length) {
|
|
||||||
isEqual = currentCreds.every(cred => {
|
|
||||||
return defaultCreds.some(item => item.id === cred.id);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
isEqual = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEqual) {
|
|
||||||
overrides.credentials = resource?.summary_fields?.credentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_diff_mode_on_launch) {
|
|
||||||
if (defaults.diff_mode !== resource.diff_mode) {
|
|
||||||
overrides.diff_mode = resource.diff_mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_inventory_on_launch) {
|
|
||||||
if (defaults.inventory.id !== resource.inventory) {
|
|
||||||
overrides.inventory = resource?.summary_fields?.inventory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_job_type_on_launch) {
|
|
||||||
if (defaults.job_type !== resource.job_type) {
|
|
||||||
overrides.job_type = resource.job_type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_limit_on_launch) {
|
|
||||||
if (defaults.limit !== resource.limit) {
|
|
||||||
overrides.limit = resource.limit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_scm_branch_on_launch) {
|
|
||||||
if (defaults.scm_branch !== resource.scm_branch) {
|
|
||||||
overrides.scm_branch = resource.scm_branch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_skip_tags_on_launch) {
|
|
||||||
if (defaults.skip_tags !== resource.skip_tags) {
|
|
||||||
overrides.skip_tags = resource.skip_tags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_tags_on_launch) {
|
|
||||||
if (defaults.job_tags !== resource.job_tags) {
|
|
||||||
overrides.job_tags = resource.job_tags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_variables_on_launch) {
|
|
||||||
if (defaults.extra_vars !== resource.extra_vars) {
|
|
||||||
overrides.extra_vars = resource.extra_vars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (launchConfig.ask_verbosity_on_launch) {
|
|
||||||
if (defaults.verbosity !== resource.verbosity) {
|
|
||||||
overrides.verbosity = resource.verbosity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const withoutOverrides = omitOverrides(resource, overrides);
|
|
||||||
|
|
||||||
return [withoutOverrides, overrides];
|
|
||||||
}
|
|
||||||
|
|
||||||
function PromptDetail({ i18n, resource, launchConfig = {} }) {
|
|
||||||
const VERBOSITY = {
|
const VERBOSITY = {
|
||||||
0: i18n._(t`0 (Normal)`),
|
0: i18n._(t`0 (Normal)`),
|
||||||
1: i18n._(t`1 (Verbose)`),
|
1: i18n._(t`1 (Verbose)`),
|
||||||
@@ -160,7 +86,7 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
|
|||||||
4: i18n._(t`4 (Connection Debug)`),
|
4: i18n._(t`4 (Connection Debug)`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [details, overrides] = partitionPromptDetails(resource, launchConfig);
|
const details = omitOverrides(resource, overrides);
|
||||||
const hasOverrides = Object.keys(overrides).length > 0;
|
const hasOverrides = Object.keys(overrides).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe('PromptDetail', () => {
|
|||||||
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(wrapper.find('PromptDetail h2').text()).toBe('Prompted Values');
|
expect(wrapper.find('PromptDetail h2')).toHaveLength(0);
|
||||||
assertDetail('Name', 'Mock JT');
|
assertDetail('Name', 'Mock JT');
|
||||||
assertDetail('Description', 'Mock JT Description');
|
assertDetail('Description', 'Mock JT Description');
|
||||||
assertDetail('Type', 'Job Template');
|
assertDetail('Type', 'Job Template');
|
||||||
@@ -143,4 +143,74 @@ describe('PromptDetail', () => {
|
|||||||
expect(overrideDetails.find('VariablesDetail').length).toBe(0);
|
expect(overrideDetails.find('VariablesDetail').length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with overrides', () => {
|
||||||
|
let wrapper;
|
||||||
|
const overrides = {
|
||||||
|
extra_vars: '---one: two\nbar: baz',
|
||||||
|
inventory: {
|
||||||
|
name: 'Override inventory',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<PromptDetail
|
||||||
|
launchConfig={mockPromptLaunch}
|
||||||
|
resource={{
|
||||||
|
...mockTemplate,
|
||||||
|
ask_inventory_on_launch: true,
|
||||||
|
}}
|
||||||
|
overrides={overrides}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render overridden details', () => {
|
||||||
|
function assertDetail(label, value) {
|
||||||
|
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||||
|
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(wrapper.find('PromptDetail h2').text()).toBe('Prompted Values');
|
||||||
|
assertDetail('Name', 'Mock JT');
|
||||||
|
assertDetail('Description', 'Mock JT Description');
|
||||||
|
assertDetail('Type', 'Job Template');
|
||||||
|
assertDetail('Job Type', 'Run');
|
||||||
|
assertDetail('Inventory', 'Override inventory');
|
||||||
|
assertDetail('Source Control Branch', 'Foo branch');
|
||||||
|
assertDetail('Limit', 'alpha:beta');
|
||||||
|
assertDetail('Verbosity', '3 (Debug)');
|
||||||
|
assertDetail('Show Changes', 'Off');
|
||||||
|
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||||
|
'---one: two\nbar: baz'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('Detail[label="Credentials"]')
|
||||||
|
.containsAllMatchingElements([
|
||||||
|
<span>
|
||||||
|
<strong>SSH:</strong>Credential 1
|
||||||
|
</span>,
|
||||||
|
<span>
|
||||||
|
<strong>Awx:</strong>Credential 2
|
||||||
|
</span>,
|
||||||
|
])
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('Detail[label="Job Tags"]')
|
||||||
|
.containsAnyMatchingElements([<span>T_100</span>, <span>T_200</span>])
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('Detail[label="Skip Tags"]')
|
||||||
|
.containsAllMatchingElements([<span>S_100</span>, <span>S_200</span>])
|
||||||
|
).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -306,3 +306,21 @@ export const Schedule = shape({
|
|||||||
timezone: string,
|
timezone: string,
|
||||||
until: string,
|
until: string,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SurveyQuestion = shape({
|
||||||
|
question_name: string,
|
||||||
|
question_description: string,
|
||||||
|
required: bool,
|
||||||
|
type: string,
|
||||||
|
variable: string,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
default: string,
|
||||||
|
choices: string,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Survey = shape({
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
spec: arrayOf(SurveyQuestion),
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user