mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
addresses missing name property and fixes tests
This commit is contained in:
parent
7d5b198ce6
commit
307c9eafb3
@ -44,6 +44,7 @@ class LaunchButton extends React.Component {
|
||||
showLaunchPrompt: false,
|
||||
launchConfig: null,
|
||||
launchError: false,
|
||||
surveyConfig: null,
|
||||
};
|
||||
|
||||
this.handleLaunch = this.handleLaunch.bind(this);
|
||||
@ -67,15 +68,28 @@ class LaunchButton extends React.Component {
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
||||
: JobTemplatesAPI.readLaunch(resource.id);
|
||||
const readSurvey =
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readSurvey(resource.id)
|
||||
: JobTemplatesAPI.readSurvey(resource.id);
|
||||
try {
|
||||
const { data: launchConfig } = await readLaunch;
|
||||
|
||||
let surveyConfig = null;
|
||||
|
||||
if (launchConfig.survey_enabled) {
|
||||
const { data } = await readSurvey;
|
||||
|
||||
surveyConfig = data;
|
||||
}
|
||||
|
||||
if (canLaunchWithoutPrompt(launchConfig)) {
|
||||
this.launchWithParams({});
|
||||
} else {
|
||||
this.setState({
|
||||
showLaunchPrompt: true,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -151,7 +165,12 @@ class LaunchButton extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { launchError, showLaunchPrompt, launchConfig } = this.state;
|
||||
const {
|
||||
launchError,
|
||||
showLaunchPrompt,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
} = this.state;
|
||||
const { resource, i18n, children } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
@ -172,7 +191,8 @@ class LaunchButton extends React.Component {
|
||||
)}
|
||||
{showLaunchPrompt && (
|
||||
<LaunchPrompt
|
||||
config={launchConfig}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resource={resource}
|
||||
onLaunch={this.launchWithParams}
|
||||
onCancel={() => this.setState({ showLaunchPrompt: false })}
|
||||
|
||||
@ -11,7 +11,14 @@ import useLaunchSteps from './useLaunchSteps';
|
||||
import AlertModal from '../AlertModal';
|
||||
import getSurveyValues from './getSurveyValues';
|
||||
|
||||
function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
||||
function PromptModalForm({
|
||||
launchConfig,
|
||||
i18n,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
resource,
|
||||
surveyConfig,
|
||||
}) {
|
||||
const { values, setTouched, validateForm } = useFormikContext();
|
||||
|
||||
const {
|
||||
@ -20,7 +27,7 @@ function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
||||
visitStep,
|
||||
visitAllSteps,
|
||||
contentError,
|
||||
} = useLaunchSteps(config, resource, i18n);
|
||||
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
||||
|
||||
const handleSave = () => {
|
||||
const postValues = {};
|
||||
@ -39,7 +46,7 @@ function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
||||
setValue('limit', values.limit);
|
||||
setValue('job_tags', values.job_tags);
|
||||
setValue('skip_tags', values.skip_tags);
|
||||
const extraVars = config.ask_variables_on_launch
|
||||
const extraVars = launchConfig.ask_variables_on_launch
|
||||
? values.extra_vars || '---'
|
||||
: resource.extra_vars;
|
||||
setValue('extra_vars', mergeExtraVars(extraVars, surveyValues));
|
||||
@ -103,33 +110,22 @@ function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
||||
);
|
||||
}
|
||||
|
||||
function LaunchPrompt({ config, resource = {}, onLaunch, onCancel, i18n }) {
|
||||
function LaunchPrompt({
|
||||
launchConfig,
|
||||
i18n,
|
||||
onCancel,
|
||||
onLaunch,
|
||||
resource = {},
|
||||
surveyConfig,
|
||||
}) {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
verbosity: resource.verbosity || 0,
|
||||
inventory:
|
||||
resource.summary_fields?.inventory || null,
|
||||
credentials:
|
||||
resource.summary_fields?.credentials || [],
|
||||
diff_mode:
|
||||
config.ask_diff_mode_on_launch && (resource.diff_mode || false),
|
||||
extra_vars:
|
||||
resource.extra_vars || '---',
|
||||
job_type: resource.job_type || 'run',
|
||||
job_tags: resource.job_tags || '',
|
||||
skip_tags: resource.skip_tags || '',
|
||||
scm_branch:
|
||||
resource.scm_branch || '',
|
||||
limit: resource.limit || '',
|
||||
}}
|
||||
onSubmit={values => onLaunch(values)}
|
||||
>
|
||||
<Formik initialValues={{}} onSubmit={values => onLaunch(values)}>
|
||||
<PromptModalForm
|
||||
onSubmit={values => onLaunch(values)}
|
||||
onCancel={onCancel}
|
||||
i18n={i18n}
|
||||
config={config}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resource={resource}
|
||||
/>
|
||||
</Formik>
|
||||
|
||||
@ -76,7 +76,7 @@ describe('LaunchPrompt', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LaunchPrompt
|
||||
config={{
|
||||
launchConfig={{
|
||||
...config,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: true,
|
||||
@ -86,6 +86,24 @@ describe('LaunchPrompt', () => {
|
||||
resource={resource}
|
||||
onLaunch={noop}
|
||||
onCancel={noop}
|
||||
surveyConfig={{
|
||||
name: '',
|
||||
description: '',
|
||||
spec: [
|
||||
{
|
||||
choices: '',
|
||||
default: '',
|
||||
max: 1024,
|
||||
min: 0,
|
||||
new_question: false,
|
||||
question_description: '',
|
||||
question_name: 'foo',
|
||||
required: true,
|
||||
type: 'text',
|
||||
variable: 'foo',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -105,7 +123,7 @@ describe('LaunchPrompt', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LaunchPrompt
|
||||
config={{
|
||||
launchConfig={{
|
||||
...config,
|
||||
ask_inventory_on_launch: true,
|
||||
}}
|
||||
@ -129,7 +147,7 @@ describe('LaunchPrompt', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LaunchPrompt
|
||||
config={{
|
||||
launchConfig={{
|
||||
...config,
|
||||
ask_credential_on_launch: true,
|
||||
}}
|
||||
@ -153,7 +171,7 @@ describe('LaunchPrompt', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<LaunchPrompt
|
||||
config={{
|
||||
launchConfig={{
|
||||
...config,
|
||||
ask_verbosity_on_launch: true,
|
||||
}}
|
||||
|
||||
@ -20,11 +20,11 @@ const FieldHeader = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
function OtherPromptsStep({ config, i18n }) {
|
||||
function OtherPromptsStep({ launchConfig, i18n }) {
|
||||
return (
|
||||
<Form>
|
||||
{config.ask_job_type_on_launch && <JobTypeField i18n={i18n} />}
|
||||
{config.ask_limit_on_launch && (
|
||||
{launchConfig.ask_job_type_on_launch && <JobTypeField i18n={i18n} />}
|
||||
{launchConfig.ask_limit_on_launch && (
|
||||
<FormField
|
||||
id="prompt-limit"
|
||||
name="limit"
|
||||
@ -35,7 +35,7 @@ function OtherPromptsStep({ config, i18n }) {
|
||||
information and examples on patterns.`)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_scm_branch_on_launch && (
|
||||
{launchConfig.ask_scm_branch_on_launch && (
|
||||
<FormField
|
||||
id="prompt-scm-branch"
|
||||
name="scm_branch"
|
||||
@ -45,9 +45,11 @@ function OtherPromptsStep({ config, i18n }) {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_verbosity_on_launch && <VerbosityField i18n={i18n} />}
|
||||
{config.ask_diff_mode_on_launch && <ShowChangesToggle i18n={i18n} />}
|
||||
{config.ask_tags_on_launch && (
|
||||
{launchConfig.ask_verbosity_on_launch && <VerbosityField i18n={i18n} />}
|
||||
{launchConfig.ask_diff_mode_on_launch && (
|
||||
<ShowChangesToggle i18n={i18n} />
|
||||
)}
|
||||
{launchConfig.ask_tags_on_launch && (
|
||||
<TagField
|
||||
id="prompt-job-tags"
|
||||
name="job_tags"
|
||||
@ -59,7 +61,7 @@ function OtherPromptsStep({ config, i18n }) {
|
||||
documentation for details on the usage of tags.`)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_skip_tags_on_launch && (
|
||||
{launchConfig.ask_skip_tags_on_launch && (
|
||||
<TagField
|
||||
id="prompt-skip-tags"
|
||||
name="skip_tags"
|
||||
@ -71,7 +73,7 @@ function OtherPromptsStep({ config, i18n }) {
|
||||
documentation for details on the usage of tags.`)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_variables_on_launch && (
|
||||
{launchConfig.ask_variables_on_launch && (
|
||||
<VariablesField
|
||||
id="prompt-variables"
|
||||
name="extra_vars"
|
||||
|
||||
@ -11,7 +11,7 @@ describe('OtherPromptsStep', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ job_type: 'run' }}>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_job_type_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
@ -34,7 +34,7 @@ describe('OtherPromptsStep', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_limit_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
@ -54,7 +54,7 @@ describe('OtherPromptsStep', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_scm_branch_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
@ -74,7 +74,7 @@ describe('OtherPromptsStep', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ verbosity: '' }}>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_verbosity_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
@ -94,7 +94,7 @@ describe('OtherPromptsStep', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ diff_mode: true }}>
|
||||
<OtherPromptsStep
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_diff_mode_on_launch: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -24,18 +24,25 @@ const ErrorMessageWrapper = styled.div`
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
||||
function PreviewStep({
|
||||
resource,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
formErrors,
|
||||
i18n,
|
||||
}) {
|
||||
const { values } = useFormikContext();
|
||||
const surveyValues = getSurveyValues(values);
|
||||
|
||||
const overrides = {
|
||||
...values,
|
||||
};
|
||||
if (config.ask_variables_on_launch || config.survey_enabled) {
|
||||
|
||||
if (launchConfig.ask_variables_on_launch || launchConfig.survey_enabled) {
|
||||
const initialExtraVars =
|
||||
config.ask_variables_on_launch && (overrides.extra_vars || '---');
|
||||
if (survey && survey.spec) {
|
||||
const passwordFields = survey.spec
|
||||
launchConfig.ask_variables_on_launch && (overrides.extra_vars || '---');
|
||||
if (surveyConfig?.spec) {
|
||||
const passwordFields = surveyConfig.spec
|
||||
.filter(q => q.type === 'password')
|
||||
.map(q => q.variable);
|
||||
const masked = maskPasswords(surveyValues, passwordFields);
|
||||
@ -46,11 +53,10 @@ function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
||||
overrides.extra_vars = initialExtraVars;
|
||||
}
|
||||
}
|
||||
// Api expects extra vars to be merged with the survey data.
|
||||
// We put the extra_data key/value pair on the values object here
|
||||
// so that we don't have to do this loop again inside of the NodeAddModal.jsx
|
||||
|
||||
values.extra_data =
|
||||
overrides.extra_vars && parseVariableField(overrides?.extra_vars);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{formErrors && (
|
||||
@ -67,7 +73,7 @@ function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
||||
)}
|
||||
<PromptDetail
|
||||
resource={resource}
|
||||
launchConfig={config}
|
||||
launchConfig={launchConfig}
|
||||
overrides={overrides}
|
||||
/>
|
||||
</Fragment>
|
||||
|
||||
@ -36,11 +36,11 @@ describe('PreviewStep', () => {
|
||||
<Formik initialValues={{ limit: '4', survey_foo: 'abc' }}>
|
||||
<PreviewStep
|
||||
resource={resource}
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_limit_on_launch: true,
|
||||
survey_enabled: true,
|
||||
}}
|
||||
survey={survey}
|
||||
surveyConfig={survey}
|
||||
formErrors={formErrors}
|
||||
/>
|
||||
</Formik>
|
||||
@ -64,7 +64,7 @@ describe('PreviewStep', () => {
|
||||
<Formik initialValues={{ limit: '4' }}>
|
||||
<PreviewStep
|
||||
resource={resource}
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_limit_on_launch: true,
|
||||
}}
|
||||
formErrors={formErrors}
|
||||
@ -80,7 +80,32 @@ describe('PreviewStep', () => {
|
||||
limit: '4',
|
||||
});
|
||||
});
|
||||
test('should handle extra vars with survey', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ extra_vars: 'one: 1', survey_foo: 'abc' }}>
|
||||
<PreviewStep
|
||||
resource={resource}
|
||||
launchConfig={{
|
||||
ask_variables_on_launch: true,
|
||||
survey_enabled: true,
|
||||
}}
|
||||
surveyConfig={survey}
|
||||
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\nfoo: abc\n',
|
||||
survey_foo: 'abc',
|
||||
});
|
||||
});
|
||||
test('should handle extra vars without survey', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
@ -88,7 +113,7 @@ describe('PreviewStep', () => {
|
||||
<Formik initialValues={{ extra_vars: 'one: 1' }}>
|
||||
<PreviewStep
|
||||
resource={resource}
|
||||
config={{
|
||||
launchConfig={{
|
||||
ask_variables_on_launch: true,
|
||||
}}
|
||||
formErrors={formErrors}
|
||||
@ -104,30 +129,30 @@ describe('PreviewStep', () => {
|
||||
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',
|
||||
});
|
||||
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}
|
||||
launchConfig={{
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from '../../../util/validators';
|
||||
import { Survey } from '../../../types';
|
||||
|
||||
function SurveyStep({ survey, i18n }) {
|
||||
function SurveyStep({ surveyConfig, i18n }) {
|
||||
const fieldTypes = {
|
||||
text: TextField,
|
||||
textarea: TextField,
|
||||
@ -34,7 +34,7 @@ function SurveyStep({ survey, i18n }) {
|
||||
};
|
||||
return (
|
||||
<Form>
|
||||
{survey.spec.map(question => {
|
||||
{surveyConfig.spec.map(question => {
|
||||
const Field = fieldTypes[question.type];
|
||||
return (
|
||||
<Field key={question.variable} question={question} i18n={i18n} />
|
||||
@ -44,7 +44,7 @@ function SurveyStep({ survey, i18n }) {
|
||||
);
|
||||
}
|
||||
SurveyStep.propTypes = {
|
||||
survey: Survey.isRequired,
|
||||
surveyConfig: Survey.isRequired,
|
||||
};
|
||||
|
||||
function TextField({ question, i18n }) {
|
||||
|
||||
@ -1,64 +1,17 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import {
|
||||
WorkflowJobTemplateNodesAPI,
|
||||
JobTemplatesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '../../../api';
|
||||
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
|
||||
const STEP_ID = 'credentials';
|
||||
|
||||
export default function useCredentialsStep(
|
||||
config,
|
||||
i18n,
|
||||
selectedResource,
|
||||
nodeToEdit
|
||||
) {
|
||||
const resource = nodeToEdit || selectedResource;
|
||||
const { request: fetchCredentials, result, error, isLoading } = useRequest(
|
||||
useCallback(async () => {
|
||||
let credentials;
|
||||
if (!nodeToEdit?.related?.credentials) {
|
||||
return {};
|
||||
}
|
||||
const {
|
||||
data: { results },
|
||||
} = await WorkflowJobTemplateNodesAPI.readCredentials(nodeToEdit.id);
|
||||
credentials = results;
|
||||
if (results.length === 0 && config?.defaults?.credentials) {
|
||||
const fetchCreds = config.job_template_data
|
||||
? JobTemplatesAPI.readDetail(config.job_template_data.id)
|
||||
: WorkflowJobTemplatesAPI.readDetail(
|
||||
config.workflow_job_template_data.id
|
||||
);
|
||||
|
||||
const {
|
||||
data: {
|
||||
summary_fields: { credentials: defaultCreds },
|
||||
},
|
||||
} = await fetchCreds;
|
||||
credentials = defaultCreds;
|
||||
}
|
||||
return credentials;
|
||||
}, [nodeToEdit, config])
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials, nodeToEdit]);
|
||||
|
||||
const validate = () => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export default function useCredentialsStep(launchConfig, resource, i18n) {
|
||||
return {
|
||||
step: getStep(config, i18n),
|
||||
initialValues: getInitialValues(config, resource, result),
|
||||
validate,
|
||||
isReady: !isLoading && !!result,
|
||||
contentError: error,
|
||||
step: getStep(launchConfig, i18n),
|
||||
initialValues: getInitialValues(launchConfig, resource),
|
||||
validate: () => ({}),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: null,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
@ -68,8 +21,8 @@ export default function useCredentialsStep(
|
||||
};
|
||||
}
|
||||
|
||||
function getStep(config, i18n) {
|
||||
if (!config.ask_credential_on_launch) {
|
||||
function getStep(launchConfig, i18n) {
|
||||
if (!launchConfig.ask_credential_on_launch) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@ -81,11 +34,12 @@ function getStep(config, i18n) {
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialValues(config, resource, result) {
|
||||
if (!config.ask_credential_on_launch) {
|
||||
function getInitialValues(launchConfig, resource) {
|
||||
if (!launchConfig.ask_credential_on_launch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
credentials: resource?.summary_fields?.credentials || result || [],
|
||||
credentials: resource?.summary_fields?.credentials || [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,23 +7,21 @@ import StepName from './StepName';
|
||||
const STEP_ID = 'inventory';
|
||||
|
||||
export default function useInventoryStep(
|
||||
config,
|
||||
launchConfig,
|
||||
resource,
|
||||
i18n,
|
||||
visitedSteps,
|
||||
selectedResource,
|
||||
nodeToEdit
|
||||
visitedSteps
|
||||
) {
|
||||
const [, meta] = useField('inventory');
|
||||
const resource = nodeToEdit?.originalNodeObject || nodeToEdit?.promptValues || selectedResource;
|
||||
const formError =
|
||||
Object.keys(visitedSteps).includes(STEP_ID) && (!meta.value || meta.error);
|
||||
|
||||
return {
|
||||
step: getStep(config, i18n, formError),
|
||||
initialValues: getInitialValues(config, resource),
|
||||
step: getStep(launchConfig, i18n, formError),
|
||||
initialValues: getInitialValues(launchConfig, resource),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: config.ask_inventory_on_launch && formError,
|
||||
formError: launchConfig.ask_inventory_on_launch && formError,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
inventory: true,
|
||||
@ -31,25 +29,24 @@ export default function useInventoryStep(
|
||||
},
|
||||
};
|
||||
}
|
||||
function getStep(config, i18n, formError) {
|
||||
if (!config.ask_inventory_on_launch) {
|
||||
function getStep(launchConfig, i18n, formError) {
|
||||
if (!launchConfig.ask_inventory_on_launch) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: STEP_ID,
|
||||
key: 3,
|
||||
name: <StepName hasErrors={formError}>{i18n._(t`Inventory`)}</StepName>,
|
||||
component: <InventoryStep i18n={i18n} />,
|
||||
enableNext: true,
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialValues(config, resource) {
|
||||
if (!config.ask_inventory_on_launch) {
|
||||
function getInitialValues(launchConfig, resource) {
|
||||
if (!launchConfig.ask_inventory_on_launch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
inventory: resource?.summary_fields?.inventory || resource?.inventory || null,
|
||||
inventory: resource?.summary_fields?.inventory || null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,16 +5,20 @@ import OtherPromptsStep from './OtherPromptsStep';
|
||||
|
||||
const STEP_ID = 'other';
|
||||
|
||||
export default function useOtherPrompt(
|
||||
config,
|
||||
i18n,
|
||||
selectedResource,
|
||||
nodeToEdit
|
||||
) {
|
||||
const resource = nodeToEdit || selectedResource;
|
||||
const getVariablesData = resource => {
|
||||
if (resource?.extra_data) {
|
||||
return jsonToYaml(JSON.stringify(resource.extra_data));
|
||||
}
|
||||
if (resource?.extra_vars && resource?.extra_vars !== '---') {
|
||||
return jsonToYaml(JSON.stringify(parseVariableField(resource.extra_vars)));
|
||||
}
|
||||
return '---';
|
||||
};
|
||||
|
||||
export default function useOtherPromptsStep(launchConfig, resource, i18n) {
|
||||
return {
|
||||
step: getStep(config, i18n),
|
||||
initialValues: getInitialValues(config, resource),
|
||||
step: getStep(launchConfig, i18n),
|
||||
initialValues: getInitialValues(launchConfig, resource),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: null,
|
||||
@ -32,73 +36,61 @@ export default function useOtherPrompt(
|
||||
};
|
||||
}
|
||||
|
||||
function getStep(config, i18n) {
|
||||
if (!shouldShowPrompt(config)) {
|
||||
function getStep(launchConfig, i18n) {
|
||||
if (!shouldShowPrompt(launchConfig)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: STEP_ID,
|
||||
key: 5,
|
||||
name: i18n._(t`Other Prompts`),
|
||||
component: <OtherPromptsStep config={config} i18n={i18n} />,
|
||||
component: <OtherPromptsStep launchConfig={launchConfig} i18n={i18n} />,
|
||||
enableNext: true,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldShowPrompt(config) {
|
||||
function shouldShowPrompt(launchConfig) {
|
||||
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
|
||||
launchConfig.ask_job_type_on_launch ||
|
||||
launchConfig.ask_limit_on_launch ||
|
||||
launchConfig.ask_verbosity_on_launch ||
|
||||
launchConfig.ask_tags_on_launch ||
|
||||
launchConfig.ask_skip_tags_on_launch ||
|
||||
launchConfig.ask_variables_on_launch ||
|
||||
launchConfig.ask_scm_branch_on_launch ||
|
||||
launchConfig.ask_diff_mode_on_launch
|
||||
);
|
||||
}
|
||||
|
||||
function getInitialValues(config, resource) {
|
||||
if (!config) {
|
||||
return {};
|
||||
function getInitialValues(launchConfig, resource) {
|
||||
const initialValues = {};
|
||||
|
||||
if (!launchConfig) {
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
const getVariablesData = () => {
|
||||
if (resource?.extra_data) {
|
||||
return jsonToYaml(JSON.stringify(resource?.extra_data));
|
||||
}
|
||||
if (resource?.extra_vars) {
|
||||
if (resource.extra_vars !== '---') {
|
||||
return jsonToYaml(
|
||||
JSON.stringify(parseVariableField(resource?.extra_vars))
|
||||
);
|
||||
}
|
||||
}
|
||||
return '---';
|
||||
};
|
||||
const initialValues = {};
|
||||
if (config.ask_job_type_on_launch) {
|
||||
if (launchConfig.ask_job_type_on_launch) {
|
||||
initialValues.job_type = resource?.job_type || '';
|
||||
}
|
||||
if (config.ask_limit_on_launch) {
|
||||
if (launchConfig.ask_limit_on_launch) {
|
||||
initialValues.limit = resource?.limit || '';
|
||||
}
|
||||
if (config.ask_verbosity_on_launch) {
|
||||
if (launchConfig.ask_verbosity_on_launch) {
|
||||
initialValues.verbosity = resource?.verbosity || 0;
|
||||
}
|
||||
if (config.ask_tags_on_launch) {
|
||||
if (launchConfig.ask_tags_on_launch) {
|
||||
initialValues.job_tags = resource?.job_tags || '';
|
||||
}
|
||||
if (config.ask_skip_tags_on_launch) {
|
||||
if (launchConfig.ask_skip_tags_on_launch) {
|
||||
initialValues.skip_tags = resource?.skip_tags || '';
|
||||
}
|
||||
if (config.ask_variables_on_launch) {
|
||||
initialValues.extra_vars = getVariablesData();
|
||||
if (launchConfig.ask_variables_on_launch) {
|
||||
initialValues.extra_vars = getVariablesData(resource);
|
||||
}
|
||||
if (config.ask_scm_branch_on_launch) {
|
||||
if (launchConfig.ask_scm_branch_on_launch) {
|
||||
initialValues.scm_branch = resource?.scm_branch || '';
|
||||
}
|
||||
if (config.ask_diff_mode_on_launch) {
|
||||
if (launchConfig.ask_diff_mode_on_launch) {
|
||||
initialValues.diff_mode = resource?.diff_mode || false;
|
||||
}
|
||||
return initialValues;
|
||||
|
||||
@ -5,28 +5,23 @@ import PreviewStep from './PreviewStep';
|
||||
const STEP_ID = 'preview';
|
||||
|
||||
export default function usePreviewStep(
|
||||
config,
|
||||
launchConfig,
|
||||
i18n,
|
||||
resource,
|
||||
survey,
|
||||
surveyConfig,
|
||||
hasErrors,
|
||||
needsPreviewStep,
|
||||
nodeToEdit
|
||||
showStep
|
||||
) {
|
||||
const showStep =
|
||||
needsPreviewStep && resource && Object.keys(config).length > 0;
|
||||
const promptResource = nodeToEdit || resource
|
||||
return {
|
||||
step: showStep
|
||||
? {
|
||||
id: STEP_ID,
|
||||
key: 7,
|
||||
name: i18n._(t`Preview`),
|
||||
component: (
|
||||
<PreviewStep
|
||||
config={config}
|
||||
resource={promptResource}
|
||||
survey={survey}
|
||||
launchConfig={launchConfig}
|
||||
resource={resource}
|
||||
surveyConfig={surveyConfig}
|
||||
formErrors={hasErrors}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -1,45 +1,25 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useFormikContext } from 'formik';
|
||||
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,
|
||||
i18n,
|
||||
visitedSteps,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resource,
|
||||
nodeToEdit
|
||||
i18n,
|
||||
visitedSteps
|
||||
) {
|
||||
const { values } = useFormikContext();
|
||||
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
|
||||
useCallback(async () => {
|
||||
if (!config.survey_enabled) {
|
||||
return {};
|
||||
}
|
||||
const { data } = config?.workflow_job_template_data
|
||||
? await WorkflowJobTemplatesAPI.readSurvey(
|
||||
config?.workflow_job_template_data?.id
|
||||
)
|
||||
: await JobTemplatesAPI.readSurvey(config?.job_template_data?.id);
|
||||
return data;
|
||||
}, [config])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSurvey();
|
||||
}, [fetchSurvey]);
|
||||
|
||||
const errors = {};
|
||||
const validate = () => {
|
||||
if (!config.survey_enabled || !survey || !survey.spec) {
|
||||
if (!launchConfig.survey_enabled || !surveyConfig?.spec) {
|
||||
return {};
|
||||
}
|
||||
survey.spec.forEach(question => {
|
||||
surveyConfig.spec.forEach(question => {
|
||||
const errMessage = validateField(
|
||||
question,
|
||||
values[`survey_${question.variable}`],
|
||||
@ -53,19 +33,19 @@ export default function useSurveyStep(
|
||||
};
|
||||
const formError = Object.keys(validate()).length > 0;
|
||||
return {
|
||||
step: getStep(config, survey, validate, i18n, visitedSteps),
|
||||
initialValues: getInitialValues(config, survey, nodeToEdit),
|
||||
step: getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps),
|
||||
initialValues: getInitialValues(launchConfig, surveyConfig, resource),
|
||||
validate,
|
||||
survey,
|
||||
isReady: !isLoading && !!survey,
|
||||
contentError: error,
|
||||
surveyConfig,
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError,
|
||||
setTouched: setFieldsTouched => {
|
||||
if (!survey || !survey.spec) {
|
||||
if (!surveyConfig?.spec) {
|
||||
return;
|
||||
}
|
||||
const fields = {};
|
||||
survey.spec.forEach(question => {
|
||||
surveyConfig.spec.forEach(question => {
|
||||
fields[`survey_${question.variable}`] = true;
|
||||
});
|
||||
setFieldsTouched(fields);
|
||||
@ -96,14 +76,13 @@ function validateField(question, value, i18n) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getStep(config, survey, validate, i18n, visitedSteps) {
|
||||
if (!config.survey_enabled) {
|
||||
function getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps) {
|
||||
if (!launchConfig.survey_enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
key: 6,
|
||||
name: (
|
||||
<StepName
|
||||
hasErrors={
|
||||
@ -114,26 +93,26 @@ function getStep(config, survey, validate, i18n, visitedSteps) {
|
||||
{i18n._(t`Survey`)}
|
||||
</StepName>
|
||||
),
|
||||
component: <SurveyStep survey={survey} i18n={i18n} />,
|
||||
component: <SurveyStep surveyConfig={surveyConfig} i18n={i18n} />,
|
||||
enableNext: true,
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialValues(config, survey, nodeToEdit) {
|
||||
if (!config.survey_enabled || !survey) {
|
||||
function getInitialValues(launchConfig, surveyConfig, resource) {
|
||||
if (!launchConfig.survey_enabled || !surveyConfig) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const values = {};
|
||||
if (survey && survey.spec) {
|
||||
survey.spec.forEach(question => {
|
||||
if (surveyConfig?.spec) {
|
||||
surveyConfig.spec.forEach(question => {
|
||||
if (question.type === 'multiselect') {
|
||||
values[`survey_${question.variable}`] = question.default.split('\n');
|
||||
} else {
|
||||
values[`survey_${question.variable}`] = question.default;
|
||||
}
|
||||
if (nodeToEdit?.extra_data) {
|
||||
Object.entries(nodeToEdit?.extra_data).forEach(([key, value]) => {
|
||||
if (resource?.extra_data) {
|
||||
Object.entries(resource.extra_data).forEach(([key, value]) => {
|
||||
if (key === question.variable) {
|
||||
if (question.type === 'multiselect') {
|
||||
values[`survey_${question.variable}`] = value;
|
||||
|
||||
@ -6,43 +6,48 @@ import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
||||
import useSurveyStep from './steps/useSurveyStep';
|
||||
import usePreviewStep from './steps/usePreviewStep';
|
||||
|
||||
export default function useLaunchSteps(config, resource, i18n) {
|
||||
export default function useLaunchSteps(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resource,
|
||||
i18n
|
||||
) {
|
||||
const [visited, setVisited] = useState({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const steps = [
|
||||
useInventoryStep(config, i18n, visited),
|
||||
useCredentialsStep(config, i18n),
|
||||
useOtherPromptsStep(config, i18n),
|
||||
useSurveyStep(config, i18n, visited),
|
||||
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||
useCredentialsStep(launchConfig, resource, i18n),
|
||||
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||
];
|
||||
const { resetForm, values: formikValues } = useFormikContext();
|
||||
const { resetForm } = useFormikContext();
|
||||
const hasErrors = steps.some(step => step.formError);
|
||||
|
||||
const surveyStepIndex = steps.findIndex(step => step.survey);
|
||||
steps.push(
|
||||
usePreviewStep(
|
||||
config,
|
||||
i18n,
|
||||
resource,
|
||||
steps[surveyStepIndex]?.survey,
|
||||
hasErrors,
|
||||
true
|
||||
)
|
||||
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
||||
);
|
||||
|
||||
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
||||
const isReady = !steps.some(s => !s.isReady);
|
||||
const stepsAreReady = !steps.some(s => !s.isReady);
|
||||
|
||||
useEffect(() => {
|
||||
if (surveyStepIndex > -1 && isReady) {
|
||||
if (stepsAreReady) {
|
||||
const initialValues = steps.reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
...cur.initialValues,
|
||||
};
|
||||
}, {});
|
||||
resetForm({
|
||||
values: {
|
||||
...formikValues,
|
||||
...steps[surveyStepIndex].initialValues,
|
||||
...initialValues,
|
||||
},
|
||||
});
|
||||
|
||||
setIsReady(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isReady]);
|
||||
}, [stepsAreReady]);
|
||||
|
||||
const stepWithError = steps.find(s => s.contentError);
|
||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||
|
||||
@ -71,11 +71,11 @@ function omitOverrides(resource, overrides, defaultConfig) {
|
||||
const clonedResource = {
|
||||
...resource,
|
||||
summary_fields: { ...resource.summary_fields },
|
||||
...defaultConfig
|
||||
...defaultConfig,
|
||||
};
|
||||
Object.keys(overrides).forEach(keyToOmit => {
|
||||
delete clonedResource[keyToOmit];
|
||||
delete clonedResource.summary_fields[keyToOmit];
|
||||
delete clonedResource?.summary_fields[keyToOmit];
|
||||
});
|
||||
return clonedResource;
|
||||
}
|
||||
@ -90,7 +90,7 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||
};
|
||||
|
||||
const details = omitOverrides(resource, overrides, launchConfig.defaults);
|
||||
details.type = overrides?.nodeType || details.type
|
||||
details.type = overrides?.nodeType || details.type;
|
||||
const hasOverrides = Object.keys(overrides).length > 0;
|
||||
|
||||
return (
|
||||
@ -139,7 +139,7 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||
<Divider css="margin-top: var(--pf-global--spacer--lg)" />
|
||||
<PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader>
|
||||
<DetailList aria-label="Prompt Overrides">
|
||||
{launchConfig.ask_job_type_on_launch && (
|
||||
{launchConfig.ask_job_type_on_launch && (
|
||||
<Detail
|
||||
label={i18n._(t`Job Type`)}
|
||||
value={toTitleCase(overrides.job_type)}
|
||||
@ -181,12 +181,13 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||
{launchConfig.ask_limit_on_launch && (
|
||||
<Detail label={i18n._(t`Limit`)} value={overrides.limit} />
|
||||
)}
|
||||
{overrides?.verbosity && launchConfig.ask_verbosity_on_launch && (
|
||||
{Object.prototype.hasOwnProperty.call(overrides, 'verbosity') &&
|
||||
launchConfig.ask_verbosity_on_launch ? (
|
||||
<Detail
|
||||
label={i18n._(t`Verbosity`)}
|
||||
value={VERBOSITY[overrides.verbosity]}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
{launchConfig.ask_tags_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
@ -194,13 +195,18 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={overrides.job_tags.split(',').length}
|
||||
totalChips={
|
||||
!overrides.job_tags || overrides.job_tags === ''
|
||||
? 0
|
||||
: overrides.job_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.job_tags.split(',').map(jobTag => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
))}
|
||||
{overrides.job_tags.length > 0 &&
|
||||
overrides.job_tags.split(',').map(jobTag => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
@ -212,13 +218,18 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={overrides.skip_tags.split(',').length}
|
||||
totalChips={
|
||||
!overrides.skip_tags || overrides.skip_tags === ''
|
||||
? 0
|
||||
: overrides.skip_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.skip_tags.split(',').map(skipTag => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
{overrides.skip_tags.length > 0 &&
|
||||
overrides.skip_tags.split(',').map(skipTag => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
@ -231,7 +242,8 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_variables_on_launch && (
|
||||
{(launchConfig.survey_enabled ||
|
||||
launchConfig.ask_variables_on_launch) && (
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
rows={4}
|
||||
|
||||
@ -18,7 +18,7 @@ const mockPromptLaunch = {
|
||||
defaults: {
|
||||
extra_vars: '---foo: bar',
|
||||
diff_mode: false,
|
||||
limit: 3,
|
||||
limit: 'localhost',
|
||||
job_tags: 'T_100,T_200',
|
||||
skip_tags: 'S_100,S_200',
|
||||
job_type: 'run',
|
||||
@ -74,7 +74,7 @@ describe('PromptDetail', () => {
|
||||
assertDetail('Job Type', 'Run');
|
||||
assertDetail('Inventory', 'Demo Inventory');
|
||||
assertDetail('Source Control Branch', 'Foo branch');
|
||||
assertDetail('Limit', 'alpha:beta');
|
||||
assertDetail('Limit', 'localhost');
|
||||
assertDetail('Verbosity', '3 (Debug)');
|
||||
assertDetail('Show Changes', 'Off');
|
||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||
@ -151,6 +151,14 @@ describe('PromptDetail', () => {
|
||||
inventory: {
|
||||
name: 'Override inventory',
|
||||
},
|
||||
credentials: mockPromptLaunch.defaults.credentials,
|
||||
job_tags: 'foo,bar',
|
||||
skip_tags: 'baz,boo',
|
||||
limit: 'otherlimit',
|
||||
verbosity: 0,
|
||||
job_type: 'check',
|
||||
scm_branch: 'Bar branch',
|
||||
diff_mode: true,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
@ -180,12 +188,12 @@ describe('PromptDetail', () => {
|
||||
assertDetail('Name', 'Mock JT');
|
||||
assertDetail('Description', 'Mock JT Description');
|
||||
assertDetail('Type', 'Job Template');
|
||||
assertDetail('Job Type', 'Run');
|
||||
assertDetail('Job Type', 'Check');
|
||||
assertDetail('Inventory', 'Override inventory');
|
||||
assertDetail('Source Control Branch', 'Foo branch');
|
||||
assertDetail('Limit', 'alpha:beta');
|
||||
assertDetail('Verbosity', '3 (Debug)');
|
||||
assertDetail('Show Changes', 'Off');
|
||||
assertDetail('Source Control Branch', 'Bar branch');
|
||||
assertDetail('Limit', 'otherlimit');
|
||||
assertDetail('Verbosity', '0 (Normal)');
|
||||
assertDetail('Show Changes', 'On');
|
||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||
'---one: two\nbar: baz'
|
||||
);
|
||||
@ -204,12 +212,12 @@ describe('PromptDetail', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Job Tags"]')
|
||||
.containsAnyMatchingElements([<span>T_100</span>, <span>T_200</span>])
|
||||
.containsAnyMatchingElements([<span>foo</span>, <span>bar</span>])
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Skip Tags"]')
|
||||
.containsAllMatchingElements([<span>S_100</span>, <span>S_200</span>])
|
||||
.containsAllMatchingElements([<span>baz</span>, <span>boo</span>])
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -133,10 +133,12 @@ function PromptJobTemplateDetail({ i18n, resource }) {
|
||||
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />
|
||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
||||
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
|
||||
<Detail
|
||||
label={i18n._(t`Show Changes`)}
|
||||
value={diff_mode ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||
/>
|
||||
{typeof diff_mode === 'boolean' && (
|
||||
<Detail
|
||||
label={i18n._(t`Show Changes`)}
|
||||
value={diff_mode ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
|
||||
<Detail label={i18n._(t`Host Config Key`)} value={host_config_key} />
|
||||
{related?.callback && (
|
||||
@ -149,7 +151,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
|
||||
label={i18n._(t`Webhook Service`)}
|
||||
value={toTitleCase(webhook_service)}
|
||||
/>
|
||||
{related.webhook_receiver && (
|
||||
{related?.webhook_receiver && (
|
||||
<Detail
|
||||
label={i18n._(t`Webhook URL`)}
|
||||
value={`${window.location.origin}${related.webhook_receiver}`}
|
||||
|
||||
@ -34,9 +34,12 @@ const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)`
|
||||
function WorkflowNodeHelp({ node, i18n }) {
|
||||
let nodeType;
|
||||
const job = node?.originalNodeObject?.summary_fields?.job;
|
||||
if (node.unifiedJobTemplate || job) {
|
||||
const type = node.unifiedJobTemplate
|
||||
? node.unifiedJobTemplate.unified_job_type || node.unifiedJobTemplate.type
|
||||
const unifiedJobTemplate =
|
||||
node?.fullUnifiedJobTemplate ||
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template;
|
||||
if (unifiedJobTemplate || job) {
|
||||
const type = unifiedJobTemplate
|
||||
? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type
|
||||
: job.type;
|
||||
switch (type) {
|
||||
case 'job_template':
|
||||
@ -113,7 +116,7 @@ function WorkflowNodeHelp({ node, i18n }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!node.unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && (
|
||||
{!unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && (
|
||||
<>
|
||||
<ResourceDeleted job={job}>
|
||||
<StyledExclamationTriangleIcon />
|
||||
@ -149,12 +152,12 @@ function WorkflowNodeHelp({ node, i18n }) {
|
||||
)}
|
||||
</GridDL>
|
||||
)}
|
||||
{node.unifiedJobTemplate && !job && (
|
||||
{unifiedJobTemplate && !job && (
|
||||
<GridDL>
|
||||
<dt>
|
||||
<b>{i18n._(t`Name`)}</b>
|
||||
</dt>
|
||||
<dd id="workflow-node-help-name">{node.unifiedJobTemplate.name}</dd>
|
||||
<dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd>
|
||||
<dt>
|
||||
<b>{i18n._(t`Type`)}</b>
|
||||
</dt>
|
||||
|
||||
@ -19,16 +19,27 @@ const CenteredPauseIcon = styled(PauseIcon)`
|
||||
`;
|
||||
|
||||
function WorkflowNodeTypeLetter({ node }) {
|
||||
if (
|
||||
!node?.fullUnifiedJobTemplate &&
|
||||
!node?.originalNodeObject?.summary_fields?.unified_job_template
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unifiedJobTemplate =
|
||||
node?.fullUnifiedJobTemplate ||
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template;
|
||||
|
||||
let nodeTypeLetter;
|
||||
if (
|
||||
(node.unifiedJobTemplate &&
|
||||
(node.unifiedJobTemplate.type ||
|
||||
node.unifiedJobTemplate.unified_job_type)) ||
|
||||
(node.job && node.job.type)
|
||||
unifiedJobTemplate.type ||
|
||||
unifiedJobTemplate.unified_job_type ||
|
||||
node?.job?.type
|
||||
) {
|
||||
const ujtType = node.unifiedJobTemplate
|
||||
? node.unifiedJobTemplate.type || node.unifiedJobTemplate.unified_job_type
|
||||
: node.job.type;
|
||||
const ujtType =
|
||||
unifiedJobTemplate.type ||
|
||||
unifiedJobTemplate.unified_job_type ||
|
||||
node.job.type;
|
||||
switch (ujtType) {
|
||||
case 'job_template':
|
||||
case 'job':
|
||||
|
||||
@ -8,7 +8,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { type: 'job_template' } }}
|
||||
node={{ fullUnifiedJobTemplate: { type: 'job_template' } }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -19,7 +19,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { unified_job_type: 'job' } }}
|
||||
node={{ fullUnifiedJobTemplate: { unified_job_type: 'job' } }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -30,7 +30,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { type: 'project' } }}
|
||||
node={{ fullUnifiedJobTemplate: { type: 'project' } }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -41,7 +41,9 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { unified_job_type: 'project_update' } }}
|
||||
node={{
|
||||
fullUnifiedJobTemplate: { unified_job_type: 'project_update' },
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -52,7 +54,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { type: 'inventory_source' } }}
|
||||
node={{ fullUnifiedJobTemplate: { type: 'inventory_source' } }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -64,7 +66,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{
|
||||
unifiedJobTemplate: { unified_job_type: 'inventory_update' },
|
||||
fullUnifiedJobTemplate: { unified_job_type: 'inventory_update' },
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
@ -76,7 +78,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { type: 'workflow_job_template' } }}
|
||||
node={{ fullUnifiedJobTemplate: { type: 'workflow_job_template' } }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -87,7 +89,9 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { unified_job_type: 'workflow_job' } }}
|
||||
node={{
|
||||
fullUnifiedJobTemplate: { unified_job_type: 'workflow_job' },
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -98,7 +102,9 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{ unifiedJobTemplate: { type: 'workflow_approval_template' } }}
|
||||
node={{
|
||||
fullUnifiedJobTemplate: { type: 'workflow_approval_template' },
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -110,7 +116,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
||||
<svg>
|
||||
<WorkflowNodeTypeLetter
|
||||
node={{
|
||||
unifiedJobTemplate: { unified_job_type: 'workflow_approval' },
|
||||
fullUnifiedJobTemplate: { unified_job_type: 'workflow_approval' },
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@ -180,7 +180,7 @@ function createNode(state, node) {
|
||||
|
||||
newNodes.push({
|
||||
id: nextNodeId,
|
||||
unifiedJobTemplate: node.nodeResource,
|
||||
fullUnifiedJobTemplate: node.nodeResource,
|
||||
isInvalidLinkTarget: false,
|
||||
promptValues: node.promptValues,
|
||||
});
|
||||
@ -407,7 +407,7 @@ function generateNodes(workflowNodes, i18n) {
|
||||
const arrayOfNodesForChart = [
|
||||
{
|
||||
id: 1,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
name: i18n._(t`START`),
|
||||
},
|
||||
},
|
||||
@ -420,8 +420,14 @@ function generateNodes(workflowNodes, i18n) {
|
||||
originalNodeObject: node,
|
||||
};
|
||||
|
||||
if (node.summary_fields.unified_job_template) {
|
||||
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
|
||||
if (
|
||||
node.summary_fields?.unified_job_template?.unified_job_type ===
|
||||
'workflow_approval'
|
||||
) {
|
||||
nodeObj.fullUnifiedJobTemplate = {
|
||||
...node.summary_fields.unified_job_template,
|
||||
type: 'workflow_approval_template',
|
||||
};
|
||||
}
|
||||
|
||||
arrayOfNodesForChart.push(nodeObj);
|
||||
@ -651,12 +657,19 @@ function updateLink(state, linkType) {
|
||||
|
||||
function updateNode(state, editedNode) {
|
||||
const { nodeToEdit, nodes } = state;
|
||||
const { nodeResource, launchConfig, promptValues } = editedNode;
|
||||
const newNodes = [...nodes];
|
||||
|
||||
const matchingNode = newNodes.find(node => node.id === nodeToEdit.id);
|
||||
matchingNode.unifiedJobTemplate = editedNode.nodeResource;
|
||||
matchingNode.fullUnifiedJobTemplate = nodeResource;
|
||||
matchingNode.isEdited = true;
|
||||
matchingNode.promptValues = editedNode.promptValues;
|
||||
matchingNode.launchConfig = launchConfig;
|
||||
|
||||
if (promptValues) {
|
||||
matchingNode.promptValues = promptValues;
|
||||
} else {
|
||||
delete matchingNode.promptValues;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
@ -671,7 +684,15 @@ function refreshNode(state, refreshedNode) {
|
||||
const newNodes = [...nodes];
|
||||
|
||||
const matchingNode = newNodes.find(node => node.id === nodeToView.id);
|
||||
matchingNode.unifiedJobTemplate = refreshedNode.nodeResource;
|
||||
|
||||
if (refreshedNode.fullUnifiedJobTemplate) {
|
||||
matchingNode.fullUnifiedJobTemplate = refreshedNode.fullUnifiedJobTemplate;
|
||||
}
|
||||
|
||||
if (refreshedNode.originalNodeCredentials) {
|
||||
matchingNode.originalNodeCredentials =
|
||||
refreshedNode.originalNodeCredentials;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@ -197,7 +197,7 @@ describe('Workflow reducer', () => {
|
||||
{
|
||||
id: 2,
|
||||
isInvalidLinkTarget: false,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 7000,
|
||||
name: 'Foo JT',
|
||||
},
|
||||
@ -281,7 +281,7 @@ describe('Workflow reducer', () => {
|
||||
{
|
||||
id: 3,
|
||||
isInvalidLinkTarget: false,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 7000,
|
||||
name: 'Foo JT',
|
||||
},
|
||||
@ -869,10 +869,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 2,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'JT 1',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
id: 4,
|
||||
@ -889,10 +885,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 4,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 3,
|
||||
name: 'JT 3',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -912,10 +904,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 2,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'JT 1',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
id: 3,
|
||||
@ -932,10 +920,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 3,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 2,
|
||||
name: 'JT 2',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -955,10 +939,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 5,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 4,
|
||||
name: 'JT 4',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
id: 3,
|
||||
@ -975,17 +955,13 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 3,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 2,
|
||||
name: 'JT 2',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
linkType: 'always',
|
||||
source: {
|
||||
id: 1,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
name: undefined,
|
||||
},
|
||||
},
|
||||
@ -1004,17 +980,13 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 2,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'JT 1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
linkType: 'always',
|
||||
source: {
|
||||
id: 1,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
name: undefined,
|
||||
},
|
||||
},
|
||||
@ -1033,10 +1005,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 5,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 4,
|
||||
name: 'JT 4',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -1044,7 +1012,7 @@ describe('Workflow reducer', () => {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
name: undefined,
|
||||
},
|
||||
},
|
||||
@ -1063,10 +1031,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 2,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'JT 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@ -1083,10 +1047,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 3,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 2,
|
||||
name: 'JT 2',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@ -1103,10 +1063,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 4,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 3,
|
||||
name: 'JT 3',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@ -1123,10 +1079,6 @@ describe('Workflow reducer', () => {
|
||||
},
|
||||
workflowMakerNodeId: 5,
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 4,
|
||||
name: 'JT 4',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1718,7 +1670,7 @@ describe('Workflow reducer', () => {
|
||||
id: 2,
|
||||
isEdited: false,
|
||||
isInvalidLinkTarget: false,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 703,
|
||||
name: 'Test JT',
|
||||
type: 'job_template',
|
||||
@ -1729,7 +1681,7 @@ describe('Workflow reducer', () => {
|
||||
id: 2,
|
||||
isEdited: false,
|
||||
isInvalidLinkTarget: false,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 703,
|
||||
name: 'Test JT',
|
||||
type: 'job_template',
|
||||
@ -1757,7 +1709,7 @@ describe('Workflow reducer', () => {
|
||||
id: 2,
|
||||
isEdited: true,
|
||||
isInvalidLinkTarget: false,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 704,
|
||||
name: 'Other JT',
|
||||
type: 'job_template',
|
||||
|
||||
@ -65,6 +65,10 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) {
|
||||
const history = useHistory();
|
||||
const { nodePositions } = useContext(WorkflowStateContext);
|
||||
const job = node?.originalNodeObject?.summary_fields?.job;
|
||||
const jobName =
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template?.name ||
|
||||
node?.unifiedJobTemplate?.name;
|
||||
|
||||
let borderColor = '#93969A';
|
||||
|
||||
if (job) {
|
||||
@ -110,19 +114,15 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) {
|
||||
{job ? (
|
||||
<>
|
||||
<JobTopLine>
|
||||
{job.status && <StatusIcon status={job.status} />}
|
||||
<p>{job.name || node.unifiedJobTemplate.name}</p>
|
||||
{job.status !== 'pending' && <StatusIcon status={job.status} />}
|
||||
<p>{jobName}</p>
|
||||
</JobTopLine>
|
||||
{!!job?.elapsed && (
|
||||
<Elapsed>{secondsToHHMMSS(job.elapsed)}</Elapsed>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<NodeDefaultLabel>
|
||||
{node.unifiedJobTemplate
|
||||
? node.unifiedJobTemplate.name
|
||||
: i18n._(t`DELETED`)}
|
||||
</NodeDefaultLabel>
|
||||
<NodeDefaultLabel>{jobName || i18n._(t`DELETED`)}</NodeDefaultLabel>
|
||||
)}
|
||||
</NodeContents>
|
||||
</foreignObject>
|
||||
|
||||
@ -14,6 +14,9 @@ const nodeWithJT = {
|
||||
status: 'successful',
|
||||
type: 'job',
|
||||
},
|
||||
unified_job_template: {
|
||||
name: 'Automation JT',
|
||||
},
|
||||
},
|
||||
unifiedJobTemplate: {
|
||||
id: 77,
|
||||
@ -34,6 +37,9 @@ const nodeWithoutJT = {
|
||||
status: 'successful',
|
||||
type: 'job',
|
||||
},
|
||||
unified_job_template: {
|
||||
name: 'Automation JT 2',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -80,7 +86,7 @@ describe('WorkflowOutputNode', () => {
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper.contains(<p>Automation JT</p>)).toEqual(true);
|
||||
expect(wrapper.text('p')).toContain('Automation JT');
|
||||
expect(wrapper.find('WorkflowOutputNode Elapsed').text()).toBe('00:00:07');
|
||||
});
|
||||
test('node contents displayed correctly when Job Template deleted', () => {
|
||||
@ -95,7 +101,7 @@ describe('WorkflowOutputNode', () => {
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper.contains(<p>Automation JT 2</p>)).toEqual(true);
|
||||
expect(wrapper.contains(<p>Automation JT 2</p>)).toBe(true);
|
||||
expect(wrapper.find('WorkflowOutputNode Elapsed').text()).toBe('00:00:07');
|
||||
});
|
||||
test('node contents displayed correctly when Job deleted', () => {
|
||||
|
||||
@ -12,8 +12,15 @@ function NodeAddModal({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { addNodeSource } = useContext(WorkflowStateContext);
|
||||
|
||||
const addNode = (values, linkType, config) => {
|
||||
const { approvalName, approvalDescription, approvalTimeout } = values;
|
||||
const addNode = (values, config) => {
|
||||
const {
|
||||
approvalName,
|
||||
approvalDescription,
|
||||
timeoutMinutes,
|
||||
timeoutSeconds,
|
||||
linkType,
|
||||
} = values;
|
||||
|
||||
if (values) {
|
||||
const { added, removed } = getAddedAndRemoved(
|
||||
config?.defaults?.credentials,
|
||||
@ -24,21 +31,21 @@ function NodeAddModal({ i18n }) {
|
||||
values.removedCredentials = removed;
|
||||
}
|
||||
|
||||
let node;
|
||||
if (values.nodeType === 'approval') {
|
||||
node = {
|
||||
nodeResource: {
|
||||
description: approvalDescription,
|
||||
name: approvalName,
|
||||
timeout: approvalTimeout,
|
||||
type: 'workflow_approval_template',
|
||||
},
|
||||
const node = {
|
||||
linkType,
|
||||
};
|
||||
|
||||
delete values.linkType;
|
||||
|
||||
if (values.nodeType === 'workflow_approval_template') {
|
||||
node.nodeResource = {
|
||||
description: approvalDescription,
|
||||
name: approvalName,
|
||||
timeout: Number(timeoutMinutes) * 60 + Number(timeoutSeconds),
|
||||
type: 'workflow_approval_template',
|
||||
};
|
||||
} else {
|
||||
node = {
|
||||
linkType,
|
||||
nodeResource: values.nodeResource,
|
||||
};
|
||||
node.nodeResource = values.nodeResource;
|
||||
if (
|
||||
values?.nodeType === 'job_template' ||
|
||||
values?.nodeType === 'workflow_job_template'
|
||||
|
||||
@ -40,7 +40,10 @@ describe('NodeAddModal', () => {
|
||||
el => el.length === 0
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('NodeModal').prop('onSave')({ nodeResource }, 'success', {});
|
||||
wrapper.find('NodeModal').prop('onSave')(
|
||||
{ linkType: 'success', nodeResource },
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
|
||||
@ -2,34 +2,47 @@ import React, { useContext } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { WorkflowDispatchContext } from '../../../../../contexts/Workflow';
|
||||
import { getAddedAndRemoved } from '../../../../../util/lists';
|
||||
import NodeModal from './NodeModal';
|
||||
|
||||
function NodeEditModal({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
|
||||
const updateNode = (values, linkType, config) => {
|
||||
const { added, removed } = getAddedAndRemoved(
|
||||
config?.defaults?.credentials,
|
||||
values?.credentials
|
||||
);
|
||||
if (added?.length > 0) {
|
||||
values.addedCredentals = added;
|
||||
}
|
||||
if (removed?.length > 0) {
|
||||
values.removedCredentals = removed;
|
||||
}
|
||||
values.inventory = values?.inventory?.id;
|
||||
delete values.linkType;
|
||||
const node = {
|
||||
nodeResource: values.nodeResource,
|
||||
};
|
||||
if (
|
||||
values?.nodeType === 'job_template' ||
|
||||
values?.nodeType === 'workflow_job_template'
|
||||
) {
|
||||
node.promptValues = values;
|
||||
const updateNode = (values, config) => {
|
||||
const {
|
||||
approvalName,
|
||||
approvalDescription,
|
||||
credentials,
|
||||
linkType,
|
||||
nodeResource,
|
||||
nodeType,
|
||||
timeoutMinutes,
|
||||
timeoutSeconds,
|
||||
...rest
|
||||
} = values;
|
||||
let node;
|
||||
if (values.nodeType === 'workflow_approval_template') {
|
||||
node = {
|
||||
nodeResource: {
|
||||
description: approvalDescription,
|
||||
name: approvalName,
|
||||
timeout: Number(timeoutMinutes) * 60 + Number(timeoutSeconds),
|
||||
type: 'workflow_approval_template',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
node = {
|
||||
nodeResource,
|
||||
};
|
||||
if (nodeType === 'job_template' || nodeType === 'workflow_job_template') {
|
||||
node.promptValues = {
|
||||
...rest,
|
||||
credentials,
|
||||
};
|
||||
|
||||
node.launchConfig = config;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_NODE',
|
||||
node,
|
||||
|
||||
@ -29,26 +29,18 @@ import AlertModal from '../../../../../components/AlertModal';
|
||||
|
||||
import NodeNextButton from './NodeNextButton';
|
||||
|
||||
function canLaunchWithoutPrompt(nodeType, launchData) {
|
||||
if (nodeType !== 'workflow_job_template' && nodeType !== 'job_template') {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
launchData.can_start_without_user_input &&
|
||||
!launchData.ask_inventory_on_launch &&
|
||||
!launchData.ask_variables_on_launch &&
|
||||
!launchData.ask_limit_on_launch &&
|
||||
!launchData.ask_scm_branch_on_launch &&
|
||||
!launchData.survey_enabled &&
|
||||
(!launchData.variables_needed_to_start ||
|
||||
launchData.variables_needed_to_start.length === 0)
|
||||
);
|
||||
}
|
||||
|
||||
function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
function NodeModalForm({
|
||||
askLinkType,
|
||||
i18n,
|
||||
onSave,
|
||||
title,
|
||||
credentialError,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
isLaunchLoading,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||
const { values, setTouched, validateForm } = useFormikContext();
|
||||
|
||||
const [triggerNext, setTriggerNext] = useState(0);
|
||||
@ -63,67 +55,28 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
history.replace(`${history.location.pathname}?${otherParts.join('&')}`);
|
||||
};
|
||||
|
||||
const {
|
||||
request: readLaunchConfig,
|
||||
error: launchConfigError,
|
||||
result: launchConfig,
|
||||
isLoading,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const readLaunch = (type, id) =>
|
||||
type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readLaunch(id)
|
||||
: JobTemplatesAPI.readLaunch(id);
|
||||
if (
|
||||
(values?.nodeType === 'workflow_job_template' &&
|
||||
values.nodeResource?.unified_job_type === 'job') ||
|
||||
(values?.nodeType === 'job_template' &&
|
||||
values.nodeResource?.unified_job_type === 'workflow_job')
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
if (
|
||||
values.nodeType === 'workflow_job_template' ||
|
||||
values.nodeType === 'job_template'
|
||||
) {
|
||||
if (values.nodeResource) {
|
||||
const { data } = await readLaunch(
|
||||
values.nodeType,
|
||||
values?.nodeResource?.id
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [values.nodeResource, values.nodeType]),
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
readLaunchConfig();
|
||||
}, [readLaunchConfig, values.nodeResource, values.nodeType]);
|
||||
|
||||
const {
|
||||
steps: promptSteps,
|
||||
isReady,
|
||||
visitStep,
|
||||
visitAllSteps,
|
||||
contentError,
|
||||
} = useWorkflowNodeSteps(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
i18n,
|
||||
values.nodeResource,
|
||||
askLinkType,
|
||||
!canLaunchWithoutPrompt(values.nodeType, launchConfig),
|
||||
nodeToEdit
|
||||
askLinkType
|
||||
);
|
||||
|
||||
const handleSaveNode = () => {
|
||||
clearQueryParams();
|
||||
onSave(values, askLinkType ? values.linkType : null, launchConfig);
|
||||
if (values.nodeType !== 'workflow_approval_template') {
|
||||
delete values.approvalName;
|
||||
delete values.approvalDescription;
|
||||
delete values.timeoutMinutes;
|
||||
delete values.timeoutSeconds;
|
||||
}
|
||||
onSave(values, launchConfig);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@ -132,30 +85,22 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
};
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
launchConfigError || contentError || credentialError
|
||||
contentError || credentialError
|
||||
);
|
||||
|
||||
const steps = [
|
||||
...(isReady
|
||||
? [...promptSteps]
|
||||
: [
|
||||
{
|
||||
name: i18n._(t`Content Loading`),
|
||||
component: <ContentLoading />,
|
||||
},
|
||||
]),
|
||||
];
|
||||
const nextButtonText = activeStep =>
|
||||
activeStep.id === steps[steps?.length - 1]?.id ||
|
||||
activeStep.id === promptSteps[promptSteps?.length - 1]?.id ||
|
||||
activeStep.name === 'Preview'
|
||||
? i18n._(t`Save`)
|
||||
: i18n._(t`Next`);
|
||||
|
||||
const CustomFooter = (
|
||||
<WizardFooter>
|
||||
<WizardContextConsumer>
|
||||
{({ activeStep, onNext, onBack }) => (
|
||||
<>
|
||||
<NodeNextButton
|
||||
isDisabled={isLaunchLoading}
|
||||
triggerNext={triggerNext}
|
||||
activeStep={activeStep}
|
||||
aria-label={nextButtonText(activeStep)}
|
||||
@ -163,7 +108,7 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
onClick={() => setTriggerNext(triggerNext + 1)}
|
||||
buttonText={nextButtonText(activeStep)}
|
||||
/>
|
||||
{activeStep && activeStep.id !== steps[0]?.id && (
|
||||
{activeStep && activeStep.id !== promptSteps[0]?.id && (
|
||||
<Button
|
||||
id="back-node-modal"
|
||||
variant="secondary"
|
||||
@ -187,11 +132,22 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
</WizardFooter>
|
||||
);
|
||||
|
||||
const wizardTitle = values.nodeResource
|
||||
? `${title} | ${values.nodeResource.name}`
|
||||
: title;
|
||||
if (error) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => {
|
||||
dismissError();
|
||||
}}
|
||||
>
|
||||
<ContentError error={error} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !isLoading) {
|
||||
if (error && !isLaunchLoading) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
@ -208,7 +164,7 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
return (
|
||||
<Wizard
|
||||
footer={CustomFooter}
|
||||
isOpen={!error || !contentError}
|
||||
isOpen={!error}
|
||||
onClose={handleCancel}
|
||||
onSave={() => {
|
||||
handleSaveNode();
|
||||
@ -221,9 +177,9 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
steps={steps}
|
||||
steps={promptSteps}
|
||||
css="overflow: scroll"
|
||||
title={wizardTitle}
|
||||
title={title}
|
||||
onNext={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setTouched);
|
||||
@ -236,24 +192,135 @@ function NodeModalForm({ askLinkType, i18n, onSave, title, credentialError }) {
|
||||
);
|
||||
}
|
||||
|
||||
const NodeModalInner = ({ i18n, title, ...rest }) => {
|
||||
const { values } = useFormikContext();
|
||||
|
||||
const wizardTitle = values.nodeResource
|
||||
? `${title} | ${values.nodeResource.name}`
|
||||
: title;
|
||||
|
||||
const {
|
||||
request: readLaunchConfigs,
|
||||
error: launchConfigError,
|
||||
result: { launchConfig, surveyConfig },
|
||||
isLoading,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const readLaunch = (type, id) =>
|
||||
type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readLaunch(id)
|
||||
: JobTemplatesAPI.readLaunch(id);
|
||||
if (
|
||||
!values.nodeResource ||
|
||||
!['job_template', 'workflow_job_template'].includes(values?.nodeType) ||
|
||||
!['job_template', 'workflow_job_template'].includes(
|
||||
values.nodeResource?.type
|
||||
)
|
||||
) {
|
||||
return {
|
||||
launchConfig: {},
|
||||
surveyConfig: {},
|
||||
};
|
||||
}
|
||||
|
||||
const { data: launch } = await readLaunch(
|
||||
values.nodeType,
|
||||
values?.nodeResource?.id
|
||||
);
|
||||
|
||||
let survey = {};
|
||||
|
||||
if (launch.survey_enabled) {
|
||||
const { data } = launch?.workflow_job_template_data
|
||||
? await WorkflowJobTemplatesAPI.readSurvey(
|
||||
launch?.workflow_job_template_data?.id
|
||||
)
|
||||
: await JobTemplatesAPI.readSurvey(launch?.job_template_data?.id);
|
||||
|
||||
survey = data;
|
||||
}
|
||||
|
||||
return {
|
||||
launchConfig: launch,
|
||||
surveyConfig: survey,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [values.nodeResource, values.nodeType]),
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
readLaunchConfigs();
|
||||
}, [readLaunchConfigs, values.nodeResource]);
|
||||
|
||||
const { error, dismissError } = useDismissableError(launchConfigError);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => {
|
||||
dismissError();
|
||||
}}
|
||||
>
|
||||
<ContentError error={error} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
|
||||
if (!launchConfig || !surveyConfig) {
|
||||
return (
|
||||
<Wizard
|
||||
isOpen
|
||||
steps={[
|
||||
{
|
||||
name: i18n._(t`Loading`),
|
||||
component: <ContentLoading />,
|
||||
},
|
||||
]}
|
||||
title={wizardTitle}
|
||||
footer={<></>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeModalForm
|
||||
{...rest}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
isLaunchLoading={isLoading}
|
||||
title={wizardTitle}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeModal = ({ onSave, i18n, askLinkType, title }) => {
|
||||
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||
const onSaveForm = (values, linkType, config) => {
|
||||
onSave(values, linkType, config);
|
||||
const onSaveForm = (values, config) => {
|
||||
onSave(values, config);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
nodeResource:
|
||||
nodeToEdit?.originalNodeObject?.summary_fields
|
||||
?.unified_job_template || null,
|
||||
approvalName: '',
|
||||
approvalDescription: '',
|
||||
timeoutMinutes: 0,
|
||||
timeoutSeconds: 0,
|
||||
linkType: 'success',
|
||||
nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null,
|
||||
nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template',
|
||||
}}
|
||||
onSave={() => onSaveForm}
|
||||
>
|
||||
{formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<NodeModalForm
|
||||
<NodeModalInner
|
||||
onSave={onSaveForm}
|
||||
i18n={i18n}
|
||||
title={title}
|
||||
|
||||
@ -16,6 +16,8 @@ import {
|
||||
} from '../../../../../api';
|
||||
import NodeModal from './NodeModal';
|
||||
|
||||
jest.mock('../../../../../api/models/Credentials');
|
||||
jest.mock('../../../../../api/models/Inventories');
|
||||
jest.mock('../../../../../api/models/InventorySources');
|
||||
jest.mock('../../../../../api/models/JobTemplates');
|
||||
jest.mock('../../../../../api/models/Projects');
|
||||
@ -25,23 +27,81 @@ let wrapper;
|
||||
const dispatch = jest.fn();
|
||||
const onSave = jest.fn();
|
||||
|
||||
const jtLaunchConfig = {
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: true,
|
||||
ask_tags_on_launch: true,
|
||||
ask_diff_mode_on_launch: true,
|
||||
ask_skip_tags_on_launch: true,
|
||||
ask_job_type_on_launch: true,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: true,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: true,
|
||||
survey_enabled: true,
|
||||
variables_needed_to_start: ['a'],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: false,
|
||||
job_template_data: {
|
||||
name: 'A User-2 has admin permission',
|
||||
id: 25,
|
||||
description: '',
|
||||
},
|
||||
defaults: {
|
||||
extra_vars: '---',
|
||||
diff_mode: false,
|
||||
limit: '',
|
||||
job_tags: '',
|
||||
skip_tags: '',
|
||||
job_type: 'run',
|
||||
verbosity: 0,
|
||||
inventory: {
|
||||
name: ' Inventory 1 Org 0',
|
||||
id: 1,
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
id: 2,
|
||||
name: ' Credential 2 User 1',
|
||||
credential_type: 1,
|
||||
passwords_needed: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'vault cred',
|
||||
credential_type: 3,
|
||||
passwords_needed: [],
|
||||
vault_id: '',
|
||||
},
|
||||
],
|
||||
scm_branch: '',
|
||||
},
|
||||
};
|
||||
|
||||
const mockJobTemplate = {
|
||||
id: 1,
|
||||
name: 'Test Job Template',
|
||||
type: 'job_template',
|
||||
url: '/api/v2/job_templates/1',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
name: 'Foo Inv',
|
||||
id: 1,
|
||||
},
|
||||
recent_jobs: [],
|
||||
},
|
||||
related: { webhook_receiver: '' },
|
||||
inventory: 1,
|
||||
};
|
||||
|
||||
describe('NodeModal', () => {
|
||||
beforeAll(() => {
|
||||
JobTemplatesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test Job Template',
|
||||
type: 'job_template',
|
||||
url: '/api/v2/job_templates/1',
|
||||
summary_fields: {
|
||||
recent_jobs: [],
|
||||
},
|
||||
related: { webhook_receiver: '' },
|
||||
},
|
||||
],
|
||||
results: [mockJobTemplate],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||
@ -53,60 +113,7 @@ describe('NodeModal', () => {
|
||||
related_search_fields: [],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: true,
|
||||
ask_tags_on_launch: true,
|
||||
ask_diff_mode_on_launch: true,
|
||||
ask_skip_tags_on_launch: true,
|
||||
ask_job_type_on_launch: true,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: true,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: true,
|
||||
survey_enabled: true,
|
||||
variables_needed_to_start: ['a'],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: false,
|
||||
job_template_data: {
|
||||
name: 'A User-2 has admin permission',
|
||||
id: 25,
|
||||
description: '',
|
||||
},
|
||||
defaults: {
|
||||
extra_vars: '---',
|
||||
diff_mode: false,
|
||||
limit: '',
|
||||
job_tags: '',
|
||||
skip_tags: '',
|
||||
job_type: 'run',
|
||||
verbosity: 0,
|
||||
inventory: {
|
||||
name: ' Inventory 1 Org 0',
|
||||
id: 1,
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
id: 2,
|
||||
name: ' Credential 2 User 1',
|
||||
credential_type: 1,
|
||||
passwords_needed: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'vault cred',
|
||||
credential_type: 3,
|
||||
passwords_needed: [],
|
||||
vault_id: '',
|
||||
},
|
||||
],
|
||||
scm_branch: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({ data: jtLaunchConfig });
|
||||
JobTemplatesAPI.readSurvey.mockResolvedValue({
|
||||
data: {
|
||||
name: '',
|
||||
@ -241,6 +248,7 @@ describe('NodeModal', () => {
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
onSave.mockClear();
|
||||
});
|
||||
|
||||
test('Can successfully create a new job template node', async () => {
|
||||
@ -251,7 +259,10 @@ describe('NodeModal', () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('Radio').simulate('click');
|
||||
await act(async () => {
|
||||
wrapper.find('Radio').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
});
|
||||
@ -265,10 +276,24 @@ describe('NodeModal', () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Preview"]')
|
||||
.find('a')
|
||||
.find('button')
|
||||
.prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(JobTemplatesAPI.readLaunch).toBeCalledWith(1);
|
||||
expect(JobTemplatesAPI.readSurvey).toBeCalledWith(25);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('NodeNextButton').prop('buttonText')).toBe('Save');
|
||||
act(() => {
|
||||
wrapper.find('NodeNextButton').prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
@ -276,19 +301,20 @@ describe('NodeModal', () => {
|
||||
expect(onSave).toBeCalledWith(
|
||||
{
|
||||
linkType: 'always',
|
||||
nodeResource: {
|
||||
id: 1,
|
||||
name: 'Test Job Template',
|
||||
related: { webhook_receiver: '' },
|
||||
summary_fields: { recent_jobs: [] },
|
||||
type: 'job_template',
|
||||
url: '/api/v2/job_templates/1',
|
||||
},
|
||||
nodeType: 'job_template',
|
||||
verbosity: undefined,
|
||||
inventory: { name: 'Foo Inv', id: 1 },
|
||||
credentials: [],
|
||||
job_type: '',
|
||||
verbosity: '0',
|
||||
job_tags: '',
|
||||
skip_tags: '',
|
||||
extra_vars: '---',
|
||||
diff_mode: false,
|
||||
survey_bar: 'answer',
|
||||
nodeResource: mockJobTemplate,
|
||||
extra_data: { bar: 'answer' },
|
||||
},
|
||||
'always',
|
||||
{}
|
||||
jtLaunchConfig
|
||||
);
|
||||
});
|
||||
|
||||
@ -301,10 +327,12 @@ describe('NodeModal', () => {
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect').prop('onChange')(null, 'project_sync');
|
||||
wrapper.find('AnsibleSelect').prop('onChange')(null, 'project');
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('Radio').simulate('click');
|
||||
await act(async () => {
|
||||
wrapper.find('Radio').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
@ -318,10 +346,9 @@ describe('NodeModal', () => {
|
||||
type: 'project',
|
||||
url: '/api/v2/projects/1',
|
||||
},
|
||||
nodeType: 'project_sync',
|
||||
nodeType: 'project',
|
||||
verbosity: undefined,
|
||||
},
|
||||
'failure',
|
||||
{}
|
||||
);
|
||||
});
|
||||
@ -337,11 +364,13 @@ describe('NodeModal', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect').prop('onChange')(
|
||||
null,
|
||||
'inventory_source_sync'
|
||||
'inventory_source'
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('Radio').simulate('click');
|
||||
await act(async () => {
|
||||
wrapper.find('Radio').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
@ -355,10 +384,9 @@ describe('NodeModal', () => {
|
||||
type: 'inventory_source',
|
||||
url: '/api/v2/inventory_sources/1',
|
||||
},
|
||||
nodeType: 'inventory_source_sync',
|
||||
nodeType: 'inventory_source',
|
||||
verbosity: undefined,
|
||||
},
|
||||
'failure',
|
||||
{}
|
||||
);
|
||||
});
|
||||
@ -398,7 +426,6 @@ describe('NodeModal', () => {
|
||||
nodeType: 'workflow_job_template',
|
||||
verbosity: undefined,
|
||||
},
|
||||
'success',
|
||||
{
|
||||
ask_inventory_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
@ -429,7 +456,10 @@ describe('NodeModal', () => {
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect').prop('onChange')(null, 'approval');
|
||||
wrapper.find('AnsibleSelect').prop('onChange')(
|
||||
null,
|
||||
'workflow_approval_template'
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
@ -446,12 +476,6 @@ describe('NodeModal', () => {
|
||||
wrapper.find('input#approval-timeout-minutes').simulate('change', {
|
||||
target: { value: 5, name: 'timeoutMinutes' },
|
||||
});
|
||||
});
|
||||
|
||||
// Updating the minutes and seconds is split to avoid a race condition.
|
||||
// They both update the same state variable in the parent so triggering
|
||||
// them syncronously creates flakey test results.
|
||||
await act(async () => {
|
||||
wrapper.find('input#approval-timeout-seconds').simulate('change', {
|
||||
target: { value: 30, name: 'timeoutSeconds' },
|
||||
});
|
||||
@ -480,11 +504,10 @@ describe('NodeModal', () => {
|
||||
approvalName: 'Test Approval',
|
||||
linkType: 'always',
|
||||
nodeResource: null,
|
||||
nodeType: 'approval',
|
||||
timeout: 330,
|
||||
verbosity: undefined,
|
||||
nodeType: 'workflow_approval_template',
|
||||
timeoutMinutes: 5,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
'always',
|
||||
{}
|
||||
);
|
||||
});
|
||||
@ -511,10 +534,10 @@ describe('NodeModal', () => {
|
||||
value={{
|
||||
nodeToEdit: {
|
||||
id: 2,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Test Project',
|
||||
unified_job_type: 'project_update',
|
||||
type: 'project',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@ -530,11 +553,12 @@ describe('NodeModal', () => {
|
||||
});
|
||||
await waitForElement(newWrapper, 'PFWizard');
|
||||
newWrapper.update();
|
||||
expect(newWrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||
'project_sync'
|
||||
);
|
||||
expect(newWrapper.find('AnsibleSelect').prop('value')).toBe('project');
|
||||
await act(async () => {
|
||||
newWrapper.find('AnsibleSelect').prop('onChange')(null, 'approval');
|
||||
newWrapper.find('AnsibleSelect').prop('onChange')(
|
||||
null,
|
||||
'workflow_approval_template'
|
||||
);
|
||||
});
|
||||
newWrapper.update();
|
||||
await act(async () => {
|
||||
@ -550,12 +574,6 @@ describe('NodeModal', () => {
|
||||
newWrapper.find('input#approval-timeout-minutes').simulate('change', {
|
||||
target: { value: 5, name: 'timeoutMinutes' },
|
||||
});
|
||||
});
|
||||
|
||||
// Updating the minutes and seconds is split to avoid a race condition.
|
||||
// They both update the same state variable in the parent so triggering
|
||||
// them syncronously creates flakey test results.
|
||||
await act(async () => {
|
||||
newWrapper.find('input#approval-timeout-seconds').simulate('change', {
|
||||
target: { value: 30, name: 'timeoutSeconds' },
|
||||
});
|
||||
@ -584,11 +602,10 @@ describe('NodeModal', () => {
|
||||
approvalName: 'Test Approval',
|
||||
linkType: 'success',
|
||||
nodeResource: null,
|
||||
nodeType: 'approval',
|
||||
timeout: 330,
|
||||
verbosity: undefined,
|
||||
nodeType: 'workflow_approval_template',
|
||||
timeoutMinutes: 5,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
null,
|
||||
{}
|
||||
);
|
||||
});
|
||||
@ -601,11 +618,11 @@ describe('NodeModal', () => {
|
||||
value={{
|
||||
nodeToEdit: {
|
||||
id: 2,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Test Approval',
|
||||
description: 'Test Approval Description',
|
||||
unified_job_type: 'workflow_approval',
|
||||
type: 'workflow_approval_template',
|
||||
timeout: 0,
|
||||
},
|
||||
},
|
||||
@ -621,7 +638,9 @@ describe('NodeModal', () => {
|
||||
);
|
||||
});
|
||||
await waitForElement(newWrapper, 'PFWizard');
|
||||
expect(newWrapper.find('AnsibleSelect').prop('value')).toBe('approval');
|
||||
expect(newWrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||
'workflow_approval_template'
|
||||
);
|
||||
await act(async () => {
|
||||
newWrapper.find('AnsibleSelect').prop('onChange')(
|
||||
null,
|
||||
@ -649,9 +668,7 @@ describe('NodeModal', () => {
|
||||
url: '/api/v2/workflow_job_templates/1',
|
||||
},
|
||||
nodeType: 'workflow_job_template',
|
||||
verbosity: undefined,
|
||||
},
|
||||
null,
|
||||
{
|
||||
ask_inventory_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
|
||||
@ -8,6 +8,7 @@ function NodeNextButton({
|
||||
onClick,
|
||||
onNext,
|
||||
triggerNext,
|
||||
isDisabled,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!triggerNext) {
|
||||
@ -22,7 +23,7 @@ function NodeNextButton({
|
||||
variant="primary"
|
||||
type="submit"
|
||||
onClick={() => onClick(activeStep)}
|
||||
isDisabled={!activeStep.enableNext}
|
||||
isDisabled={isDisabled || !activeStep.enableNext}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'styled-components/macro';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
@ -28,13 +28,16 @@ const TimeoutLabel = styled.p`
|
||||
`;
|
||||
|
||||
function NodeTypeStep({ i18n }) {
|
||||
const [timeoutMinutes, setTimeoutMinutes] = useState(0);
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState(0);
|
||||
const [nodeTypeField, , nodeTypeHelpers] = useField('nodeType');
|
||||
const [nodeResourceField, , nodeResourceHelpers] = useField('nodeResource');
|
||||
const [, approvalNameMeta, approvalNameHelpers] = useField('approvalName');
|
||||
const [, , approvalDescriptionHelpers] = useField('approvalDescription');
|
||||
const [, , timeoutHelpers] = useField('timeout');
|
||||
const [timeoutMinutesField, , timeoutMinutesHelpers] = useField(
|
||||
'timeoutMinutes'
|
||||
);
|
||||
const [timeoutSecondsField, , timeoutSecondsHelpers] = useField(
|
||||
'timeoutSeconds'
|
||||
);
|
||||
|
||||
const isValid = !approvalNameMeta.touched || !approvalNameMeta.error;
|
||||
return (
|
||||
@ -47,14 +50,14 @@ function NodeTypeStep({ i18n }) {
|
||||
label={i18n._(t`Select a Node Type`)}
|
||||
data={[
|
||||
{
|
||||
key: 'approval',
|
||||
value: 'approval',
|
||||
key: 'workflow_approval_template',
|
||||
value: 'workflow_approval_template',
|
||||
label: i18n._(t`Approval`),
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
key: 'inventory_source_sync',
|
||||
value: 'inventory_source_sync',
|
||||
key: 'inventory_source',
|
||||
value: 'inventory_source',
|
||||
label: i18n._(t`Inventory Source Sync`),
|
||||
isDisabled: false,
|
||||
},
|
||||
@ -65,8 +68,8 @@ function NodeTypeStep({ i18n }) {
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
key: 'project_sync',
|
||||
value: 'project_sync',
|
||||
key: 'project',
|
||||
value: 'project',
|
||||
label: i18n._(t`Project Sync`),
|
||||
isDisabled: false,
|
||||
},
|
||||
@ -83,7 +86,8 @@ function NodeTypeStep({ i18n }) {
|
||||
nodeResourceHelpers.setValue(null);
|
||||
approvalNameHelpers.setValue('');
|
||||
approvalDescriptionHelpers.setValue('');
|
||||
timeoutHelpers.setValue(0);
|
||||
timeoutMinutesHelpers.setValue(0);
|
||||
timeoutSecondsHelpers.setValue(0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -94,13 +98,13 @@ function NodeTypeStep({ i18n }) {
|
||||
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||
/>
|
||||
)}
|
||||
{nodeTypeField.value === 'project_sync' && (
|
||||
{nodeTypeField.value === 'project' && (
|
||||
<ProjectsList
|
||||
nodeResource={nodeResourceField.value}
|
||||
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||
/>
|
||||
)}
|
||||
{nodeTypeField.value === 'inventory_source_sync' && (
|
||||
{nodeTypeField.value === 'inventory_source' && (
|
||||
<InventorySourcesList
|
||||
nodeResource={nodeResourceField.value}
|
||||
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||
@ -112,12 +116,11 @@ function NodeTypeStep({ i18n }) {
|
||||
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||
/>
|
||||
)}
|
||||
{nodeTypeField.value === 'approval' && (
|
||||
{nodeTypeField.value === 'workflow_approval_template' && (
|
||||
<Form css="margin-top: 20px;">
|
||||
<FormFullWidthLayout>
|
||||
<FormField
|
||||
name="approvalName"
|
||||
fieldId="approval-name"
|
||||
id="approval-name"
|
||||
isRequired
|
||||
validate={required(null, i18n)}
|
||||
@ -126,7 +129,6 @@ function NodeTypeStep({ i18n }) {
|
||||
/>
|
||||
<FormField
|
||||
name="approvalDescription"
|
||||
fieldId="approval-description"
|
||||
id="approval-description"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
@ -137,44 +139,29 @@ function NodeTypeStep({ i18n }) {
|
||||
>
|
||||
<div css="display: flex;align-items: center;">
|
||||
<TimeoutInput
|
||||
aria-label={i18n._(t`timeout-minutes`)}
|
||||
name="timeoutMinutes"
|
||||
{...timeoutMinutesField}
|
||||
aria-label={i18n._(t`Timeout minutes`)}
|
||||
id="approval-timeout-minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={timeoutMinutes}
|
||||
onChange={(value, evt) => {
|
||||
if (!evt.target.value || evt.target.value === '') {
|
||||
evt.target.value = 0;
|
||||
}
|
||||
setTimeoutMinutes(evt.target.value);
|
||||
timeoutHelpers.setValue(
|
||||
Number(evt.target.value) * 60 + Number(timeoutSeconds)
|
||||
);
|
||||
onChange={(value, event) => {
|
||||
timeoutMinutesField.onChange(event);
|
||||
}}
|
||||
step="1"
|
||||
type="number"
|
||||
/>
|
||||
<TimeoutLabel>
|
||||
<Trans>min</Trans>
|
||||
</TimeoutLabel>
|
||||
<TimeoutInput
|
||||
name="timeoutSeconds"
|
||||
{...timeoutSecondsField}
|
||||
aria-label={i18n._(t`Timeout seconds`)}
|
||||
id="approval-timeout-seconds"
|
||||
type="number"
|
||||
aria-label={i18n._(t`timeout-seconds`)}
|
||||
min="0"
|
||||
step="1"
|
||||
value={timeoutSeconds}
|
||||
onChange={(value, evt) => {
|
||||
if (!evt.target.value || evt.target.value === '') {
|
||||
evt.target.value = 0;
|
||||
}
|
||||
setTimeoutSeconds(evt.target.value);
|
||||
|
||||
timeoutHelpers.setValue(
|
||||
Number(evt.target.value) + Number(timeoutMinutes) * 60
|
||||
);
|
||||
onChange={(value, event) => {
|
||||
timeoutSecondsField.onChange(event);
|
||||
}}
|
||||
step="1"
|
||||
type="number"
|
||||
/>
|
||||
<TimeoutLabel>
|
||||
<Trans>sec</Trans>
|
||||
|
||||
@ -123,31 +123,31 @@ describe('NodeTypeStep', () => {
|
||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('job_template');
|
||||
expect(wrapper.find('JobTemplatesList').length).toBe(1);
|
||||
});
|
||||
test('It shows the project list when node type is project sync', async () => {
|
||||
test('It shows the project list when node type is project', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ nodeType: 'project_sync' }}>
|
||||
<Formik initialValues={{ nodeType: 'project' }}>
|
||||
<NodeTypeStep />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('project_sync');
|
||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('project');
|
||||
expect(wrapper.find('ProjectsList').length).toBe(1);
|
||||
});
|
||||
test('It shows the inventory source list when node type is inventory source sync', async () => {
|
||||
test('It shows the inventory source list when node type is inventory source', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={{ nodeType: 'inventory_source_sync' }}>
|
||||
<Formik initialValues={{ nodeType: 'inventory_source' }}>
|
||||
<NodeTypeStep />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||
'inventory_source_sync'
|
||||
'inventory_source'
|
||||
);
|
||||
expect(wrapper.find('InventorySourcesList').length).toBe(1);
|
||||
});
|
||||
@ -172,10 +172,11 @@ describe('NodeTypeStep', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik
|
||||
initialValues={{
|
||||
nodeType: 'approval',
|
||||
nodeType: 'workflow_approval_template',
|
||||
approvalName: '',
|
||||
approvalDescription: '',
|
||||
timeout: '',
|
||||
timeoutMinutes: 0,
|
||||
timeoutSeconds: 0,
|
||||
}}
|
||||
>
|
||||
<NodeTypeStep />
|
||||
@ -183,7 +184,9 @@ describe('NodeTypeStep', () => {
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('approval');
|
||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||
'workflow_approval_template'
|
||||
);
|
||||
expect(wrapper.find('FormField[label="Name"]').length).toBe(1);
|
||||
expect(wrapper.find('FormField[label="Description"]').length).toBe(1);
|
||||
expect(wrapper.find('input[name="timeoutMinutes"]').length).toBe(1);
|
||||
|
||||
@ -5,7 +5,7 @@ import NodeTypeStep from './NodeTypeStep';
|
||||
|
||||
const STEP_ID = 'nodeType';
|
||||
|
||||
export default function useNodeTypeStep(i18n, nodeToEdit) {
|
||||
export default function useNodeTypeStep(i18n) {
|
||||
const [, meta] = useField('nodeType');
|
||||
const [approvalNameField] = useField('approvalName');
|
||||
const [nodeTypeField, ,] = useField('nodeType');
|
||||
@ -13,7 +13,7 @@ export default function useNodeTypeStep(i18n, nodeToEdit) {
|
||||
|
||||
return {
|
||||
step: getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField),
|
||||
initialValues: getInitialValues(nodeToEdit),
|
||||
initialValues: getInitialValues(),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: meta.error,
|
||||
@ -27,8 +27,9 @@ export default function useNodeTypeStep(i18n, nodeToEdit) {
|
||||
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||
const isEnabled = () => {
|
||||
if (
|
||||
(nodeTypeField.value !== 'approval' && nodeResourceField.value === null) ||
|
||||
(nodeTypeField.value === 'approval' &&
|
||||
(nodeTypeField.value !== 'workflow_approval_template' &&
|
||||
nodeResourceField.value === null) ||
|
||||
(nodeTypeField.value === 'workflow_approval_template' &&
|
||||
approvalNameField.value === undefined)
|
||||
) {
|
||||
return false;
|
||||
@ -37,57 +38,18 @@ function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||
};
|
||||
return {
|
||||
id: STEP_ID,
|
||||
key: 3,
|
||||
name: i18n._(t`Node Type`),
|
||||
component: <NodeTypeStep i18n={i18n} />,
|
||||
enableNext: isEnabled(),
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialValues(nodeToEdit) {
|
||||
let typeOfNode;
|
||||
if (
|
||||
!nodeToEdit?.unifiedJobTemplate?.type &&
|
||||
!nodeToEdit?.unifiedJobTemplate?.unified_job_type
|
||||
) {
|
||||
return { nodeType: 'job_template' };
|
||||
}
|
||||
const {
|
||||
unifiedJobTemplate: { type, unified_job_type },
|
||||
} = nodeToEdit;
|
||||
const unifiedType = type || unified_job_type;
|
||||
|
||||
if (unifiedType === 'job' || unifiedType === 'job_template')
|
||||
typeOfNode = {
|
||||
nodeType: 'job_template',
|
||||
nodeResource:
|
||||
nodeToEdit.originalNodeObject?.summary_fields?.unified_job_template
|
||||
|| nodeToEdit.unifiedJobTemplate,
|
||||
};
|
||||
if (unifiedType === 'project' || unifiedType === 'project_update') {
|
||||
typeOfNode = { nodeType: 'project_sync' };
|
||||
}
|
||||
if (
|
||||
unifiedType === 'inventory_source' ||
|
||||
unifiedType === 'inventory_update'
|
||||
) {
|
||||
typeOfNode = { nodeType: 'inventory_source_sync' };
|
||||
}
|
||||
if (
|
||||
unifiedType === 'workflow_job' ||
|
||||
unifiedType === 'workflow_job_template'
|
||||
) {
|
||||
typeOfNode = {
|
||||
nodeType: 'workflow_job_template',
|
||||
};
|
||||
}
|
||||
if (
|
||||
unifiedType === 'workflow_approval_template' ||
|
||||
unifiedType === 'workflow_approval'
|
||||
) {
|
||||
typeOfNode = {
|
||||
nodeType: 'approval',
|
||||
};
|
||||
}
|
||||
return typeOfNode;
|
||||
function getInitialValues() {
|
||||
return {
|
||||
approvalName: '',
|
||||
approvalDescription: '',
|
||||
timeoutMinutes: 0,
|
||||
timeoutSeconds: 0,
|
||||
nodeType: 'job_template',
|
||||
};
|
||||
}
|
||||
|
||||
@ -11,41 +11,27 @@ import ContentError from '../../../../../components/ContentError';
|
||||
import ContentLoading from '../../../../../components/ContentLoading';
|
||||
import PromptDetail from '../../../../../components/PromptDetail';
|
||||
import useRequest from '../../../../../util/useRequest';
|
||||
import {
|
||||
InventorySourcesAPI,
|
||||
JobTemplatesAPI,
|
||||
ProjectsAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '../../../../../api';
|
||||
|
||||
function getNodeType(node) {
|
||||
const ujtType = node.type || node.unified_job_type;
|
||||
switch (ujtType) {
|
||||
case 'job_template':
|
||||
case 'job':
|
||||
return ['job_template', JobTemplatesAPI];
|
||||
case 'project':
|
||||
case 'project_update':
|
||||
return ['project_sync', ProjectsAPI];
|
||||
case 'inventory_source':
|
||||
case 'inventory_update':
|
||||
return ['inventory_source_sync', InventorySourcesAPI];
|
||||
case 'workflow_job_template':
|
||||
case 'workflow_job':
|
||||
return ['workflow_job_template', WorkflowJobTemplatesAPI];
|
||||
case 'workflow_approval_template':
|
||||
case 'workflow_approval':
|
||||
return ['approval', null];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
import { jsonToYaml } from '../../../../../util/yaml';
|
||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../../../api';
|
||||
import getNodeType from '../../shared/WorkflowJobTemplateVisualizerUtils';
|
||||
|
||||
function NodeViewModal({ i18n, readOnly }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { nodeToView } = useContext(WorkflowStateContext);
|
||||
const { unifiedJobTemplate } = nodeToView;
|
||||
const [nodeType, nodeAPI] = getNodeType(unifiedJobTemplate);
|
||||
const {
|
||||
fullUnifiedJobTemplate,
|
||||
originalNodeCredentials,
|
||||
originalNodeObject,
|
||||
promptValues,
|
||||
} = nodeToView;
|
||||
const [nodeType, nodeAPI] = getNodeType(
|
||||
fullUnifiedJobTemplate ||
|
||||
originalNodeObject?.summary_fields?.unified_job_template
|
||||
);
|
||||
|
||||
const id =
|
||||
fullUnifiedJobTemplate?.id ||
|
||||
originalNodeObject?.summary_fields?.unified_job_template.id;
|
||||
|
||||
const {
|
||||
result: launchConfig,
|
||||
@ -56,39 +42,44 @@ function NodeViewModal({ i18n, readOnly }) {
|
||||
useCallback(async () => {
|
||||
const readLaunch =
|
||||
nodeType === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readLaunch(unifiedJobTemplate.id)
|
||||
: JobTemplatesAPI.readLaunch(unifiedJobTemplate.id);
|
||||
? WorkflowJobTemplatesAPI.readLaunch(id)
|
||||
: JobTemplatesAPI.readLaunch(id);
|
||||
const { data } = await readLaunch;
|
||||
return data;
|
||||
}, [nodeType, unifiedJobTemplate.id]),
|
||||
}, [nodeType, id]),
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
result: nodeDetail,
|
||||
isLoading: isNodeDetailLoading,
|
||||
error: nodeDetailError,
|
||||
request: fetchNodeDetail,
|
||||
result: relatedData,
|
||||
isLoading: isRelatedDataLoading,
|
||||
error: relatedDataError,
|
||||
request: fetchRelatedData,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
let { data } = await nodeAPI?.readDetail(unifiedJobTemplate.id);
|
||||
|
||||
if (data?.type === 'job_template') {
|
||||
const related = {};
|
||||
if (
|
||||
nodeType === 'job_template' &&
|
||||
!fullUnifiedJobTemplate.instance_groups
|
||||
) {
|
||||
const {
|
||||
data: { results = [] },
|
||||
} = await JobTemplatesAPI.readInstanceGroups(data.id);
|
||||
data = Object.assign(data, { instance_groups: results });
|
||||
} = await JobTemplatesAPI.readInstanceGroups(fullUnifiedJobTemplate.id);
|
||||
related.instance_groups = results;
|
||||
}
|
||||
|
||||
if (data?.related?.webhook_receiver) {
|
||||
if (
|
||||
fullUnifiedJobTemplate?.related?.webhook_receiver &&
|
||||
!fullUnifiedJobTemplate.webhook_key
|
||||
) {
|
||||
const {
|
||||
data: { webhook_key },
|
||||
} = await nodeAPI?.readWebhookKey(data.id);
|
||||
data = Object.assign(data, { webhook_key });
|
||||
} = await nodeAPI?.readWebhookKey(fullUnifiedJobTemplate.id);
|
||||
related.webhook_key = webhook_key;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [nodeAPI, unifiedJobTemplate.id]),
|
||||
return related;
|
||||
}, [nodeAPI, fullUnifiedJobTemplate, nodeType]),
|
||||
null
|
||||
);
|
||||
|
||||
@ -97,21 +88,27 @@ function NodeViewModal({ i18n, readOnly }) {
|
||||
fetchLaunchConfig();
|
||||
}
|
||||
|
||||
if (unifiedJobTemplate.unified_job_type && nodeType !== 'approval') {
|
||||
fetchNodeDetail();
|
||||
if (
|
||||
fullUnifiedJobTemplate &&
|
||||
((nodeType === 'job_template' &&
|
||||
!fullUnifiedJobTemplate.instance_groups) ||
|
||||
(fullUnifiedJobTemplate?.related?.webhook_receiver &&
|
||||
!fullUnifiedJobTemplate.webhook_key))
|
||||
) {
|
||||
fetchRelatedData();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeDetail) {
|
||||
if (relatedData) {
|
||||
dispatch({
|
||||
type: 'REFRESH_NODE',
|
||||
node: {
|
||||
nodeResource: nodeDetail,
|
||||
fullUnifiedJobTemplate: { ...fullUnifiedJobTemplate, ...relatedData },
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [nodeDetail]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [relatedData]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleEdit = () => {
|
||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: null });
|
||||
@ -119,13 +116,74 @@ function NodeViewModal({ i18n, readOnly }) {
|
||||
};
|
||||
|
||||
let Content;
|
||||
if (isLaunchConfigLoading || isNodeDetailLoading) {
|
||||
if (isLaunchConfigLoading || isRelatedDataLoading) {
|
||||
Content = <ContentLoading />;
|
||||
} else if (launchConfigError || nodeDetailError) {
|
||||
Content = <ContentError error={launchConfigError || nodeDetailError} />;
|
||||
} else {
|
||||
} else if (launchConfigError || relatedDataError) {
|
||||
Content = <ContentError error={launchConfigError || relatedDataError} />;
|
||||
} else if (!fullUnifiedJobTemplate) {
|
||||
Content = (
|
||||
<PromptDetail launchConfig={launchConfig} resource={unifiedJobTemplate} />
|
||||
<p>
|
||||
{i18n._(t`The resource associated with this node has been deleted.`)}
|
||||
|
||||
{!readOnly
|
||||
? i18n._(t`Click the Edit button below to reconfigure the node.`)
|
||||
: ''}
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
let overrides = {};
|
||||
|
||||
if (promptValues) {
|
||||
overrides = promptValues;
|
||||
|
||||
if (launchConfig.ask_variables_on_launch || launchConfig.survey_enabled) {
|
||||
overrides.extra_vars = jsonToYaml(
|
||||
JSON.stringify(promptValues.extra_data)
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
fullUnifiedJobTemplate.id === originalNodeObject?.unified_job_template
|
||||
) {
|
||||
if (launchConfig.ask_inventory_on_launch) {
|
||||
overrides.inventory = originalNodeObject.summary_fields.inventory;
|
||||
}
|
||||
if (launchConfig.ask_scm_branch_on_launch) {
|
||||
overrides.scm_branch = originalNodeObject.scm_branch;
|
||||
}
|
||||
if (launchConfig.ask_variables_on_launch || launchConfig.survey_enabled) {
|
||||
overrides.extra_vars = jsonToYaml(
|
||||
JSON.stringify(originalNodeObject.extra_data)
|
||||
);
|
||||
}
|
||||
if (launchConfig.ask_tags_on_launch) {
|
||||
overrides.job_tags = originalNodeObject.job_tags;
|
||||
}
|
||||
if (launchConfig.ask_diff_mode_on_launch) {
|
||||
overrides.diff_mode = originalNodeObject.diff_mode;
|
||||
}
|
||||
if (launchConfig.ask_skip_tags_on_launch) {
|
||||
overrides.skip_tags = originalNodeObject.skip_tags;
|
||||
}
|
||||
if (launchConfig.ask_job_type_on_launch) {
|
||||
overrides.job_type = originalNodeObject.job_type;
|
||||
}
|
||||
if (launchConfig.ask_limit_on_launch) {
|
||||
overrides.limit = originalNodeObject.limit;
|
||||
}
|
||||
if (launchConfig.ask_verbosity_on_launch) {
|
||||
overrides.verbosity = originalNodeObject.verbosity.toString();
|
||||
}
|
||||
if (launchConfig.ask_credential_on_launch) {
|
||||
overrides.credentials = originalNodeCredentials || [];
|
||||
}
|
||||
}
|
||||
|
||||
Content = (
|
||||
<PromptDetail
|
||||
launchConfig={launchConfig}
|
||||
resource={fullUnifiedJobTemplate}
|
||||
overrides={overrides}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -133,7 +191,7 @@ function NodeViewModal({ i18n, readOnly }) {
|
||||
<Modal
|
||||
variant="large"
|
||||
isOpen
|
||||
title={unifiedJobTemplate.name}
|
||||
title={fullUnifiedJobTemplate?.name || i18n._(t`Resource deleted`)}
|
||||
aria-label={i18n._(t`Workflow node view modal`)}
|
||||
onClose={() => dispatch({ type: 'SET_NODE_TO_VIEW', value: null })}
|
||||
actions={
|
||||
|
||||
@ -53,13 +53,16 @@ describe('NodeViewModal', () => {
|
||||
let wrapper;
|
||||
const workflowContext = {
|
||||
nodeToView: {
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Mock Node',
|
||||
description: '',
|
||||
unified_job_type: 'workflow_job',
|
||||
created: '2019-08-08T19:24:05.344276Z',
|
||||
modified: '2019-08-08T19:24:18.162949Z',
|
||||
related: {
|
||||
webhook_receiver: '/api/v2/workflow_job_templates/2/github/',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -88,7 +91,6 @@ describe('NodeViewModal', () => {
|
||||
|
||||
test('should fetch workflow template launch data', () => {
|
||||
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readDetail).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
|
||||
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||
expect(WorkflowJobTemplatesAPI.readWebhookKey).toHaveBeenCalledWith(1);
|
||||
@ -118,7 +120,7 @@ describe('NodeViewModal', () => {
|
||||
describe('Job template node', () => {
|
||||
const workflowContext = {
|
||||
nodeToView: {
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Mock Node',
|
||||
description: '',
|
||||
@ -145,7 +147,6 @@ describe('NodeViewModal', () => {
|
||||
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readWebhookKey).not.toHaveBeenCalledWith();
|
||||
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||
expect(JobTemplatesAPI.readDetail).toHaveBeenCalledWith(1);
|
||||
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
@ -207,7 +208,7 @@ describe('NodeViewModal', () => {
|
||||
describe('Project node', () => {
|
||||
const workflowContext = {
|
||||
nodeToView: {
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Mock Node',
|
||||
description: '',
|
||||
@ -237,4 +238,71 @@ describe('NodeViewModal', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inventory Source node', () => {
|
||||
const workflowContext = {
|
||||
nodeToView: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Mock Node',
|
||||
description: '',
|
||||
type: 'inventory_source',
|
||||
created: '2019-08-08T19:24:05.344276Z',
|
||||
modified: '2019-08-08T19:24:18.162949Z',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should not fetch launch data', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||
<WorkflowStateContext.Provider value={workflowContext}>
|
||||
<NodeViewModal />
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
);
|
||||
});
|
||||
waitForLoaded(wrapper);
|
||||
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Approval node', () => {
|
||||
const workflowContext = {
|
||||
nodeToView: {
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Mock Node',
|
||||
description: '',
|
||||
type: 'workflow_approval_template',
|
||||
timeout: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should not fetch launch data', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||
<WorkflowStateContext.Provider value={workflowContext}>
|
||||
<NodeViewModal />
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
);
|
||||
});
|
||||
waitForLoaded(wrapper);
|
||||
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||
expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -28,7 +28,6 @@ function getStep(askLinkType, meta, i18n) {
|
||||
}
|
||||
return {
|
||||
id: STEP_ID,
|
||||
key: 1,
|
||||
name: i18n._(t`Run Type`),
|
||||
component: <RunStep />,
|
||||
enableNext: meta.value !== '',
|
||||
|
||||
@ -1,88 +1,263 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useContext, useState, useEffect } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import useInventoryStep from '../../../../../components/LaunchPrompt/steps/useInventoryStep';
|
||||
import useCredentialsStep from '../../../../../components/LaunchPrompt/steps/useCredentialsStep';
|
||||
import useOtherPromptsStep from '../../../../../components/LaunchPrompt/steps/useOtherPromptsStep';
|
||||
import useSurveyStep from '../../../../../components/LaunchPrompt/steps/useSurveyStep';
|
||||
import usePreviewStep from '../../../../../components/LaunchPrompt/steps/usePreviewStep';
|
||||
import { WorkflowStateContext } from '../../../../../contexts/Workflow';
|
||||
import { jsonToYaml } from '../../../../../util/yaml';
|
||||
import useNodeTypeStep from './NodeTypeStep/useNodeTypeStep';
|
||||
import useRunTypeStep from './useRunTypeStep';
|
||||
|
||||
function showPreviewStep(nodeType, launchConfig) {
|
||||
if (
|
||||
!['workflow_job_template', 'job_template'].includes(nodeType) ||
|
||||
Object.keys(launchConfig).length === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!launchConfig.can_start_without_user_input ||
|
||||
launchConfig.ask_inventory_on_launch ||
|
||||
launchConfig.ask_variables_on_launch ||
|
||||
launchConfig.ask_limit_on_launch ||
|
||||
launchConfig.ask_scm_branch_on_launch ||
|
||||
launchConfig.survey_enabled ||
|
||||
(launchConfig.variables_needed_to_start &&
|
||||
launchConfig.variables_needed_to_start.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
const getNodeToEditDefaultValues = (launchConfig, surveyConfig, nodeToEdit) => {
|
||||
const initialValues = {
|
||||
nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null,
|
||||
nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template',
|
||||
};
|
||||
|
||||
if (
|
||||
nodeToEdit?.fullUnifiedJobTemplate?.type === 'workflow_approval_template'
|
||||
) {
|
||||
const timeout = nodeToEdit.fullUnifiedJobTemplate.timeout || 0;
|
||||
initialValues.approvalName = nodeToEdit.fullUnifiedJobTemplate.name || '';
|
||||
initialValues.approvalDescription =
|
||||
nodeToEdit.fullUnifiedJobTemplate.description || '';
|
||||
initialValues.timeoutMinutes = Math.floor(timeout / 60);
|
||||
initialValues.timeoutSeconds = timeout - Math.floor(timeout / 60) * 60;
|
||||
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
if (!launchConfig || launchConfig === {}) {
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
if (launchConfig.ask_inventory_on_launch) {
|
||||
// We also need to handle the case where the UJT has been deleted.
|
||||
if (nodeToEdit?.promptValues) {
|
||||
initialValues.inventory = nodeToEdit?.promptValues?.inventory;
|
||||
} else if (nodeToEdit?.originalNodeObject?.summary_fields?.inventory) {
|
||||
initialValues.inventory =
|
||||
nodeToEdit?.originalNodeObject?.summary_fields?.inventory;
|
||||
} else {
|
||||
initialValues.inventory = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (launchConfig.ask_credential_on_launch) {
|
||||
if (nodeToEdit?.promptValues?.credentials) {
|
||||
initialValues.credentials = nodeToEdit?.promptValues?.credentials;
|
||||
} else if (nodeToEdit?.originalNodeCredentials) {
|
||||
const defaultCredsWithoutOverrides = [];
|
||||
|
||||
const credentialHasScheduleOverride = templateDefaultCred => {
|
||||
let credentialHasOverride = false;
|
||||
nodeToEdit.originalNodeCredentials.forEach(scheduleCred => {
|
||||
if (
|
||||
templateDefaultCred.credential_type === scheduleCred.credential_type
|
||||
) {
|
||||
if (
|
||||
(!templateDefaultCred.vault_id &&
|
||||
!scheduleCred.inputs.vault_id) ||
|
||||
(templateDefaultCred.vault_id &&
|
||||
scheduleCred.inputs.vault_id &&
|
||||
templateDefaultCred.vault_id === scheduleCred.inputs.vault_id)
|
||||
) {
|
||||
credentialHasOverride = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return credentialHasOverride;
|
||||
};
|
||||
|
||||
if (nodeToEdit?.fullUnifiedJobTemplate?.summary_fields?.credentials) {
|
||||
nodeToEdit.fullUnifiedJobTemplate.summary_fields.credentials.forEach(
|
||||
defaultCred => {
|
||||
if (!credentialHasScheduleOverride(defaultCred)) {
|
||||
defaultCredsWithoutOverrides.push(defaultCred);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
initialValues.credentials = nodeToEdit.originalNodeCredentials.concat(
|
||||
defaultCredsWithoutOverrides
|
||||
);
|
||||
} else {
|
||||
initialValues.credentials = [];
|
||||
}
|
||||
}
|
||||
|
||||
const sourceOfValues =
|
||||
nodeToEdit?.promptValues || nodeToEdit.originalNodeObject;
|
||||
|
||||
if (launchConfig.ask_job_type_on_launch) {
|
||||
initialValues.job_type = sourceOfValues?.job_type || '';
|
||||
}
|
||||
if (launchConfig.ask_limit_on_launch) {
|
||||
initialValues.limit = sourceOfValues?.limit || '';
|
||||
}
|
||||
if (launchConfig.ask_verbosity_on_launch) {
|
||||
initialValues.verbosity = sourceOfValues?.verbosity || 0;
|
||||
}
|
||||
if (launchConfig.ask_tags_on_launch) {
|
||||
initialValues.job_tags = sourceOfValues?.job_tags || '';
|
||||
}
|
||||
if (launchConfig.ask_skip_tags_on_launch) {
|
||||
initialValues.skip_tags = sourceOfValues?.skip_tags || '';
|
||||
}
|
||||
if (launchConfig.ask_scm_branch_on_launch) {
|
||||
initialValues.scm_branch = sourceOfValues?.scm_branch || '';
|
||||
}
|
||||
if (launchConfig.ask_diff_mode_on_launch) {
|
||||
initialValues.diff_mode = sourceOfValues?.diff_mode || false;
|
||||
}
|
||||
|
||||
if (launchConfig.ask_variables_on_launch && launchConfig.survey_enabled) {
|
||||
if (nodeToEdit?.promptValues?.extra_vars) {
|
||||
initialValues.extra_vars = nodeToEdit.promptValues.extra_vars;
|
||||
} else {
|
||||
const newExtraData = { ...nodeToEdit.originalNodeObject.extra_data };
|
||||
if (surveyConfig.spec) {
|
||||
surveyConfig.spec.forEach(question => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
newExtraData,
|
||||
question.variable
|
||||
)
|
||||
) {
|
||||
delete newExtraData[question.variable];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initialValues.extra_vars = jsonToYaml(JSON.stringify(newExtraData));
|
||||
}
|
||||
}
|
||||
|
||||
if (surveyConfig?.spec) {
|
||||
surveyConfig.spec.forEach(question => {
|
||||
if (question.type === 'multiselect') {
|
||||
initialValues[`survey_${question.variable}`] = question.default.split(
|
||||
'\n'
|
||||
);
|
||||
} else {
|
||||
initialValues[`survey_${question.variable}`] = question.default;
|
||||
}
|
||||
if (sourceOfValues?.extra_data) {
|
||||
Object.entries(sourceOfValues?.extra_data).forEach(([key, value]) => {
|
||||
if (key === question.variable) {
|
||||
if (question.type === 'multiselect') {
|
||||
initialValues[`survey_${question.variable}`] = value;
|
||||
} else {
|
||||
initialValues[`survey_${question.variable}`] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
};
|
||||
|
||||
export default function useWorkflowNodeSteps(
|
||||
config,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
i18n,
|
||||
resource,
|
||||
askLinkType,
|
||||
needsPreviewStep,
|
||||
nodeToEdit
|
||||
askLinkType
|
||||
) {
|
||||
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||
const { resetForm, values: formikValues } = useFormikContext();
|
||||
const [visited, setVisited] = useState({});
|
||||
|
||||
const steps = [
|
||||
useRunTypeStep(i18n, askLinkType),
|
||||
useNodeTypeStep(i18n, nodeToEdit),
|
||||
useInventoryStep(
|
||||
config,
|
||||
i18n,
|
||||
visited,
|
||||
resource,
|
||||
nodeToEdit
|
||||
),
|
||||
useCredentialsStep(config, i18n, resource, nodeToEdit?.originalNodeObject),
|
||||
useOtherPromptsStep(config, i18n, resource, nodeToEdit?.originalNodeObject),
|
||||
useSurveyStep(
|
||||
config,
|
||||
i18n,
|
||||
visited,
|
||||
resource,
|
||||
nodeToEdit?.originalNodeObject
|
||||
),
|
||||
useNodeTypeStep(i18n),
|
||||
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||
useCredentialsStep(launchConfig, resource, i18n),
|
||||
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||
];
|
||||
|
||||
const { resetForm, values: formikValues } = useFormikContext();
|
||||
const hasErrors = steps.some(step => step.formError);
|
||||
const surveyStepIndex = steps.findIndex(step => step.survey);
|
||||
steps.push(
|
||||
usePreviewStep(
|
||||
config,
|
||||
launchConfig,
|
||||
i18n,
|
||||
resource,
|
||||
steps[surveyStepIndex]?.survey,
|
||||
surveyConfig,
|
||||
hasErrors,
|
||||
needsPreviewStep,
|
||||
nodeToEdit?.originalNodeObject
|
||||
showPreviewStep(formikValues.nodeType, launchConfig)
|
||||
)
|
||||
);
|
||||
|
||||
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
||||
const isReady = !steps.some(s => !s.isReady);
|
||||
const initialValues = steps.reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
...cur.initialValues,
|
||||
};
|
||||
}, {});
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady) {
|
||||
if (launchConfig && surveyConfig && isReady) {
|
||||
let initialValues = {};
|
||||
|
||||
if (
|
||||
nodeToEdit &&
|
||||
nodeToEdit?.fullUnifiedJobTemplate &&
|
||||
nodeToEdit?.fullUnifiedJobTemplate?.id === formikValues.nodeResource?.id
|
||||
) {
|
||||
initialValues = getNodeToEditDefaultValues(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
nodeToEdit
|
||||
);
|
||||
} else {
|
||||
initialValues = steps.reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
...cur.initialValues,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
nodeResource: formikValues.nodeResource || initialValues.nodeResource,
|
||||
nodeType: formikValues.nodeType || initialValues.nodeType,
|
||||
linkType: formikValues.linkType || 'success',
|
||||
nodeResource: formikValues.nodeResource,
|
||||
nodeType: formikValues.nodeType,
|
||||
linkType: formikValues.linkType,
|
||||
verbosity: initialValues?.verbosity?.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config, isReady]);
|
||||
}, [launchConfig, surveyConfig, isReady]);
|
||||
|
||||
const stepWithError = steps.find(s => s.contentError);
|
||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||
|
||||
return {
|
||||
steps: pfSteps,
|
||||
initialValues,
|
||||
isReady,
|
||||
visitStep: stepId =>
|
||||
setVisited({
|
||||
...visited,
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
} from '../../../contexts/Workflow';
|
||||
import { getAddedAndRemoved } from '../../../util/lists';
|
||||
import { layoutGraph } from '../../../components/Workflow/WorkflowUtils';
|
||||
import ContentError from '../../../components/ContentError';
|
||||
import ContentLoading from '../../../components/ContentLoading';
|
||||
@ -46,6 +47,45 @@ const Wrapper = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const getAggregatedCredentials = (
|
||||
originalNodeOverride = [],
|
||||
templateDefaultCredentials = []
|
||||
) => {
|
||||
let theArray = [];
|
||||
|
||||
const isCredentialOverriden = templateDefaultCred => {
|
||||
let credentialHasOverride = false;
|
||||
originalNodeOverride.forEach(overrideCred => {
|
||||
if (
|
||||
templateDefaultCred.credential_type === overrideCred.credential_type
|
||||
) {
|
||||
if (
|
||||
(!templateDefaultCred.vault_id && !overrideCred.inputs.vault_id) ||
|
||||
(templateDefaultCred.vault_id &&
|
||||
overrideCred.inputs.vault_id &&
|
||||
templateDefaultCred.vault_id === overrideCred.inputs.vault_id)
|
||||
) {
|
||||
credentialHasOverride = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return credentialHasOverride;
|
||||
};
|
||||
|
||||
if (templateDefaultCredentials.length > 0) {
|
||||
templateDefaultCredentials.forEach(defaultCred => {
|
||||
if (!isCredentialOverriden(defaultCred)) {
|
||||
theArray.push(defaultCred);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
theArray = theArray.concat(originalNodeOverride);
|
||||
|
||||
return theArray;
|
||||
};
|
||||
|
||||
const fetchWorkflowNodes = async (
|
||||
templateId,
|
||||
pageNo = 1,
|
||||
@ -286,7 +326,7 @@ function Visualizer({ template, i18n }) {
|
||||
WorkflowJobTemplateNodesAPI.destroy(node.originalNodeObject.id)
|
||||
);
|
||||
} else if (!node.isDeleted && !node.originalNodeObject) {
|
||||
if (node.unifiedJobTemplate.type === 'workflow_approval_template') {
|
||||
if (node.fullUnifiedJobTemplate.type === 'workflow_approval_template') {
|
||||
nodeRequests.push(
|
||||
WorkflowJobTemplatesAPI.createNode(template.id, {}).then(
|
||||
({ data }) => {
|
||||
@ -299,20 +339,20 @@ function Visualizer({ template, i18n }) {
|
||||
};
|
||||
approvalTemplateRequests.push(
|
||||
WorkflowJobTemplateNodesAPI.createApprovalTemplate(data.id, {
|
||||
name: node.unifiedJobTemplate.name,
|
||||
description: node.unifiedJobTemplate.description,
|
||||
timeout: node.unifiedJobTemplate.timeout,
|
||||
name: node.fullUnifiedJobTemplate.name,
|
||||
description: node.fullUnifiedJobTemplate.description,
|
||||
timeout: node.fullUnifiedJobTemplate.timeout,
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
node.promptValues.inventory = node.promptValues?.inventory?.id
|
||||
nodeRequests.push(
|
||||
WorkflowJobTemplatesAPI.createNode(template.id, {
|
||||
...node.promptValues,
|
||||
unified_job_template: node.unifiedJobTemplate.id,
|
||||
...node.promptValues,
|
||||
inventory: node.promptValues?.inventory?.id || null,
|
||||
unified_job_template: node.fullUnifiedJobTemplate.id,
|
||||
}).then(({ data }) => {
|
||||
node.originalNodeObject = data;
|
||||
originalLinkMap[node.id] = {
|
||||
@ -345,11 +385,7 @@ function Visualizer({ template, i18n }) {
|
||||
);
|
||||
}
|
||||
} else if (node.isEdited) {
|
||||
if (
|
||||
node.unifiedJobTemplate &&
|
||||
(node.unifiedJobTemplate.unified_job_type === 'workflow_approval' ||
|
||||
node.unifiedJobTemplate.type === 'workflow_approval_template')
|
||||
) {
|
||||
if (node.fullUnifiedJobTemplate.type === 'workflow_approval_template') {
|
||||
if (
|
||||
node.originalNodeObject.summary_fields.unified_job_template
|
||||
.unified_job_type === 'workflow_approval'
|
||||
@ -358,9 +394,9 @@ function Visualizer({ template, i18n }) {
|
||||
WorkflowApprovalTemplatesAPI.update(
|
||||
node.originalNodeObject.summary_fields.unified_job_template.id,
|
||||
{
|
||||
name: node.unifiedJobTemplate.name,
|
||||
description: node.unifiedJobTemplate.description,
|
||||
timeout: node.unifiedJobTemplate.timeout,
|
||||
name: node.fullUnifiedJobTemplate.name,
|
||||
description: node.fullUnifiedJobTemplate.description,
|
||||
timeout: node.fullUnifiedJobTemplate.timeout,
|
||||
}
|
||||
)
|
||||
);
|
||||
@ -369,32 +405,45 @@ function Visualizer({ template, i18n }) {
|
||||
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
|
||||
node.originalNodeObject.id,
|
||||
{
|
||||
name: node.unifiedJobTemplate.name,
|
||||
description: node.unifiedJobTemplate.description,
|
||||
timeout: node.unifiedJobTemplate.timeout,
|
||||
name: node.fullUnifiedJobTemplate.name,
|
||||
description: node.fullUnifiedJobTemplate.description,
|
||||
timeout: node.fullUnifiedJobTemplate.timeout,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
nodeRequests.push(
|
||||
WorkflowJobTemplateNodesAPI.update(node.originalNodeObject.id, {
|
||||
WorkflowJobTemplateNodesAPI.replace(node.originalNodeObject.id, {
|
||||
...node.promptValues,
|
||||
unified_job_template: node.unifiedJobTemplate.id,
|
||||
inventory: node.promptValues?.inventory?.id || null,
|
||||
unified_job_template: node.fullUnifiedJobTemplate.id,
|
||||
})
|
||||
);
|
||||
if (node?.promptValues?.addedCredentials?.length > 0) {
|
||||
node.promptValues.addedCredentials.forEach(cred =>
|
||||
|
||||
const {
|
||||
added: addedCredentials,
|
||||
removed: removedCredentials,
|
||||
} = getAddedAndRemoved(
|
||||
getAggregatedCredentials(
|
||||
node?.originalNodeCredentials,
|
||||
node.launchConfig?.defaults?.credentials
|
||||
),
|
||||
node.promptValues?.credentials
|
||||
);
|
||||
|
||||
if (addedCredentials.length > 0) {
|
||||
addedCredentials.forEach(cred => {
|
||||
associateCredentialRequests.push(
|
||||
WorkflowJobTemplateNodesAPI.associateCredentials(
|
||||
node.originalNodeObject.id,
|
||||
cred.id
|
||||
)
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
if (node?.promptValues?.removedCredentials?.length > 0) {
|
||||
node.promptValues.removedCredentials.forEach(cred =>
|
||||
if (removedCredentials?.length > 0) {
|
||||
removedCredentials.forEach(cred =>
|
||||
disassociateCredentialRequests.push(
|
||||
WorkflowJobTemplateNodesAPI.disassociateCredentials(
|
||||
node.originalNodeObject.id,
|
||||
|
||||
@ -64,7 +64,7 @@ const workflowContext = {
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
unifiedJobTemplate: {
|
||||
fullUnifiedJobTemplate: {
|
||||
name: 'Foo JT',
|
||||
type: 'job_template',
|
||||
},
|
||||
|
||||
@ -14,12 +14,16 @@ import {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
} from '../../../contexts/Workflow';
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import { WorkflowJobTemplateNodesAPI } from '../../../api';
|
||||
import { constants as wfConstants } from '../../../components/Workflow/WorkflowUtils';
|
||||
import {
|
||||
WorkflowActionTooltip,
|
||||
WorkflowActionTooltipItem,
|
||||
WorkflowNodeTypeLetter,
|
||||
} from '../../../components/Workflow';
|
||||
import getNodeType from './shared/WorkflowJobTemplateVisualizerUtils';
|
||||
|
||||
const NodeG = styled.g`
|
||||
pointer-events: ${props => (props.noPointerEvents ? 'none' : 'initial')};
|
||||
@ -52,13 +56,85 @@ function VisualizerNode({
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const [credentialsError, setCredentialsError] = useState(null);
|
||||
const [detailError, setDetailError] = useState(null);
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { addingLink, addLinkSourceNode, nodePositions } = useContext(
|
||||
const { addingLink, addLinkSourceNode, nodePositions, nodes } = useContext(
|
||||
WorkflowStateContext
|
||||
);
|
||||
const isAddLinkSourceNode =
|
||||
addLinkSourceNode && addLinkSourceNode.id === node.id;
|
||||
|
||||
const handleCredentialsErrorClose = () => setCredentialsError(null);
|
||||
const handleDetailErrorClose = () => setDetailError(null);
|
||||
|
||||
const updateNode = async () => {
|
||||
const updatedNodes = [...nodes];
|
||||
const updatedNode = updatedNodes.find(n => n.id === node.id);
|
||||
if (
|
||||
!node.fullUnifiedJobTemplate &&
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template
|
||||
) {
|
||||
const [, nodeAPI] = getNodeType(
|
||||
node.originalNodeObject.summary_fields.unified_job_template
|
||||
);
|
||||
try {
|
||||
const { data: fullUnifiedJobTemplate } = await nodeAPI.readDetail(
|
||||
node.originalNodeObject.unified_job_template
|
||||
);
|
||||
updatedNode.fullUnifiedJobTemplate = fullUnifiedJobTemplate;
|
||||
} catch (err) {
|
||||
setDetailError(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template
|
||||
?.unified_job_type === 'job' &&
|
||||
!node?.originalNodeCredentials
|
||||
) {
|
||||
try {
|
||||
const {
|
||||
data: { results },
|
||||
} = await WorkflowJobTemplateNodesAPI.readCredentials(
|
||||
node.originalNodeObject.id
|
||||
);
|
||||
updatedNode.originalNodeCredentials = results;
|
||||
} catch (err) {
|
||||
setCredentialsError(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'SET_NODES',
|
||||
value: updatedNodes,
|
||||
});
|
||||
|
||||
return updatedNode;
|
||||
};
|
||||
|
||||
const handleEditClick = async () => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
const nodeToEdit = await updateNode();
|
||||
|
||||
if (nodeToEdit) {
|
||||
dispatch({ type: 'SET_NODE_TO_EDIT', value: nodeToEdit });
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewClick = async () => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
const nodeToView = await updateNode();
|
||||
|
||||
if (nodeToView) {
|
||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: nodeToView });
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeMouseEnter = () => {
|
||||
ref.current.parentNode.appendChild(ref.current);
|
||||
setHovering(true);
|
||||
@ -91,11 +167,7 @@ function VisualizerNode({
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-details"
|
||||
key="details"
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: node });
|
||||
}}
|
||||
onClick={handleViewClick}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
@ -123,11 +195,7 @@ function VisualizerNode({
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-edit"
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
dispatch({ type: 'SET_NODE_TO_EDIT', value: node });
|
||||
}}
|
||||
onClick={handleEditClick}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
@ -164,57 +232,83 @@ function VisualizerNode({
|
||||
];
|
||||
|
||||
return (
|
||||
<NodeG
|
||||
id={`node-${node.id}`}
|
||||
job={node.job}
|
||||
noPointerEvents={isAddLinkSourceNode}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
ref={ref}
|
||||
transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id]
|
||||
.y - nodePositions[1].y})`}
|
||||
>
|
||||
<rect
|
||||
fill="#FFFFFF"
|
||||
height={wfConstants.nodeH}
|
||||
rx="2"
|
||||
ry="2"
|
||||
stroke={
|
||||
hovering && addingLink && !node.isInvalidLinkTarget
|
||||
? '#007ABC'
|
||||
: '#93969A'
|
||||
}
|
||||
strokeWidth="2px"
|
||||
width={wfConstants.nodeW}
|
||||
/>
|
||||
<foreignObject
|
||||
height="58"
|
||||
{...(!addingLink && {
|
||||
onMouseEnter: () => updateNodeHelp(node),
|
||||
onMouseLeave: () => updateNodeHelp(null),
|
||||
})}
|
||||
onClick={() => handleNodeClick()}
|
||||
width="178"
|
||||
x="1"
|
||||
y="1"
|
||||
<>
|
||||
<NodeG
|
||||
id={`node-${node.id}`}
|
||||
job={node.job}
|
||||
noPointerEvents={isAddLinkSourceNode}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
ref={ref}
|
||||
transform={`translate(${nodePositions[node.id].x},${nodePositions[
|
||||
node.id
|
||||
].y - nodePositions[1].y})`}
|
||||
>
|
||||
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
||||
<NodeResourceName id={`node-${node.id}-name`}>
|
||||
{node.unifiedJobTemplate
|
||||
? node.unifiedJobTemplate.name
|
||||
: i18n._(t`DELETED`)}
|
||||
</NodeResourceName>
|
||||
</NodeContents>
|
||||
</foreignObject>
|
||||
{node.unifiedJobTemplate && <WorkflowNodeTypeLetter node={node} />}
|
||||
{hovering && !addingLink && (
|
||||
<WorkflowActionTooltip
|
||||
pointX={wfConstants.nodeW}
|
||||
pointY={wfConstants.nodeH / 2}
|
||||
actions={tooltipActions}
|
||||
<rect
|
||||
fill="#FFFFFF"
|
||||
height={wfConstants.nodeH}
|
||||
rx="2"
|
||||
ry="2"
|
||||
stroke={
|
||||
hovering && addingLink && !node.isInvalidLinkTarget
|
||||
? '#007ABC'
|
||||
: '#93969A'
|
||||
}
|
||||
strokeWidth="2px"
|
||||
width={wfConstants.nodeW}
|
||||
/>
|
||||
<foreignObject
|
||||
height="58"
|
||||
{...(!addingLink && {
|
||||
onMouseEnter: () => updateNodeHelp(node),
|
||||
onMouseLeave: () => updateNodeHelp(null),
|
||||
})}
|
||||
onClick={() => handleNodeClick()}
|
||||
width="178"
|
||||
x="1"
|
||||
y="1"
|
||||
>
|
||||
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
||||
<NodeResourceName id={`node-${node.id}-name`}>
|
||||
{node?.fullUnifiedJobTemplate?.name ||
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template
|
||||
?.name ||
|
||||
i18n._(t`DELETED`)}
|
||||
</NodeResourceName>
|
||||
</NodeContents>
|
||||
</foreignObject>
|
||||
<WorkflowNodeTypeLetter node={node} />
|
||||
{hovering && !addingLink && (
|
||||
<WorkflowActionTooltip
|
||||
pointX={wfConstants.nodeW}
|
||||
pointY={wfConstants.nodeH / 2}
|
||||
actions={tooltipActions}
|
||||
/>
|
||||
)}
|
||||
</NodeG>
|
||||
{detailError && (
|
||||
<AlertModal
|
||||
isOpen={detailError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={handleDetailErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to retrieve full node resource object.`)}
|
||||
<ErrorDetail error={detailError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</NodeG>
|
||||
{credentialsError && (
|
||||
<AlertModal
|
||||
isOpen={credentialsError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={handleCredentialsErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to retrieve node credentials.`)}
|
||||
<ErrorDetail error={credentialsError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,31 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
} from '../../../contexts/Workflow';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import { JobTemplatesAPI, WorkflowJobTemplateNodesAPI } from '../../../api';
|
||||
import VisualizerNode from './VisualizerNode';
|
||||
import { asyncFlush } from '../../../setupTests';
|
||||
|
||||
jest.mock('../../../api/models/JobTemplates');
|
||||
jest.mock('../../../api/models/WorkflowJobTemplateNodes');
|
||||
|
||||
WorkflowJobTemplateNodesAPI.readCredentials.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
|
||||
const nodeWithJT = {
|
||||
id: 2,
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 77,
|
||||
name: 'Automation JT',
|
||||
type: 'job_template',
|
||||
},
|
||||
};
|
||||
|
||||
const mockedContext = {
|
||||
addingLink: false,
|
||||
@ -23,15 +44,7 @@ const mockedContext = {
|
||||
y: 40,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nodeWithJT = {
|
||||
id: 2,
|
||||
unifiedJobTemplate: {
|
||||
id: 77,
|
||||
name: 'Automation JT',
|
||||
type: 'job_template',
|
||||
},
|
||||
nodes: [nodeWithJT],
|
||||
};
|
||||
|
||||
const dispatch = jest.fn();
|
||||
@ -47,8 +60,6 @@ describe('VisualizerNode', () => {
|
||||
<WorkflowStateContext.Provider value={mockedContext}>
|
||||
<svg>
|
||||
<VisualizerNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithJT}
|
||||
readOnly={false}
|
||||
updateHelpText={updateHelpText}
|
||||
@ -59,6 +70,9 @@ describe('VisualizerNode', () => {
|
||||
</WorkflowDispatchContext.Provider>
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
@ -67,10 +81,10 @@ describe('VisualizerNode', () => {
|
||||
});
|
||||
test('Displays action tooltip on hover and updates help text on hover', () => {
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(1);
|
||||
expect(wrapper.find('WorkflowActionTooltipItem').length).toBe(5);
|
||||
wrapper.find('VisualizerNode').simulate('mouseleave');
|
||||
wrapper.find('g').simulate('mouseleave');
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
wrapper
|
||||
.find('foreignObject')
|
||||
@ -85,7 +99,7 @@ describe('VisualizerNode', () => {
|
||||
});
|
||||
|
||||
test('Add tooltip action hover/click updates help text and dispatches properly', () => {
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
wrapper.find('WorkflowActionTooltipItem#node-add').simulate('mouseenter');
|
||||
expect(updateHelpText).toHaveBeenCalledWith('Add a new node');
|
||||
wrapper.find('WorkflowActionTooltipItem#node-add').simulate('mouseleave');
|
||||
@ -98,8 +112,8 @@ describe('VisualizerNode', () => {
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
});
|
||||
|
||||
test('Edit tooltip action hover/click updates help text and dispatches properly', () => {
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
test('Edit tooltip action hover/click updates help text and dispatches properly', async () => {
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-edit')
|
||||
.simulate('mouseenter');
|
||||
@ -109,15 +123,27 @@ describe('VisualizerNode', () => {
|
||||
.simulate('mouseleave');
|
||||
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||
wrapper.find('WorkflowActionTooltipItem#node-edit').simulate('click');
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_NODE_TO_EDIT',
|
||||
value: nodeWithJT,
|
||||
});
|
||||
await asyncFlush();
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
type: 'SET_NODES',
|
||||
value: [nodeWithJT],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'SET_NODE_TO_EDIT',
|
||||
value: nodeWithJT,
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
});
|
||||
|
||||
test('Details tooltip action hover/click updates help text and dispatches properly', () => {
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
test('Details tooltip action hover/click updates help text and dispatches properly', async () => {
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-details')
|
||||
.simulate('mouseenter');
|
||||
@ -127,15 +153,27 @@ describe('VisualizerNode', () => {
|
||||
.simulate('mouseleave');
|
||||
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||
wrapper.find('WorkflowActionTooltipItem#node-details').simulate('click');
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_NODE_TO_VIEW',
|
||||
value: nodeWithJT,
|
||||
});
|
||||
await asyncFlush();
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
type: 'SET_NODES',
|
||||
value: [nodeWithJT],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'SET_NODE_TO_VIEW',
|
||||
value: nodeWithJT,
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
});
|
||||
|
||||
test('Link tooltip action hover/click updates help text and dispatches properly', () => {
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-link')
|
||||
.simulate('mouseenter');
|
||||
@ -153,7 +191,7 @@ describe('VisualizerNode', () => {
|
||||
});
|
||||
|
||||
test('Delete tooltip action hover/click updates help text and dispatches properly', () => {
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-delete')
|
||||
.simulate('mouseenter');
|
||||
@ -201,12 +239,12 @@ describe('VisualizerNode', () => {
|
||||
});
|
||||
test('Displays correct help text when hovering over node while adding link', () => {
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
expect(updateHelpText).toHaveBeenCalledWith(
|
||||
'Click to create a new link to this node.'
|
||||
);
|
||||
wrapper.find('VisualizerNode').simulate('mouseleave');
|
||||
wrapper.find('g').simulate('mouseleave');
|
||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||
});
|
||||
@ -227,8 +265,6 @@ describe('VisualizerNode', () => {
|
||||
<svg>
|
||||
<WorkflowStateContext.Provider value={mockedContext}>
|
||||
<VisualizerNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={{
|
||||
id: 2,
|
||||
}}
|
||||
@ -243,4 +279,143 @@ describe('VisualizerNode', () => {
|
||||
expect(wrapper.find('NodeResourceName').text()).toBe('DELETED');
|
||||
});
|
||||
});
|
||||
describe('Node without full unified job template', () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||
<WorkflowStateContext.Provider value={mockedContext}>
|
||||
<svg>
|
||||
<VisualizerNode
|
||||
node={{
|
||||
id: 2,
|
||||
originalNodeObject: {
|
||||
all_parents_must_converge: false,
|
||||
always_nodes: [],
|
||||
created: '2020-11-19T21:47:55.278081Z',
|
||||
diff_mode: null,
|
||||
extra_data: {},
|
||||
failure_nodes: [],
|
||||
id: 49,
|
||||
identifier: 'f03b62c5-40f8-49e4-97c3-5bb20c91ec91',
|
||||
inventory: null,
|
||||
job_tags: null,
|
||||
job_type: null,
|
||||
limit: null,
|
||||
modified: '2020-11-19T21:47:55.278156Z',
|
||||
related: {
|
||||
credentials:
|
||||
'/api/v2/workflow_job_template_nodes/49/credentials/',
|
||||
},
|
||||
scm_branch: null,
|
||||
skip_tags: null,
|
||||
success_nodes: [],
|
||||
summary_fields: {
|
||||
workflow_job_template: { id: 15 },
|
||||
unified_job_template: {
|
||||
id: 7,
|
||||
description: '',
|
||||
name: 'Example',
|
||||
unified_job_type: 'job',
|
||||
},
|
||||
},
|
||||
type: 'workflow_job_template_node',
|
||||
unified_job_template: 7,
|
||||
url: '/api/v2/workflow_job_template_nodes/49/',
|
||||
verbosity: null,
|
||||
workflowMakerNodeId: 2,
|
||||
workflow_job_template: 15,
|
||||
},
|
||||
}}
|
||||
readOnly={false}
|
||||
updateHelpText={updateHelpText}
|
||||
updateNodeHelp={updateNodeHelp}
|
||||
/>
|
||||
</svg>
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('Attempts to fetch full unified job template on view', async () => {
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-details')
|
||||
.simulate('click');
|
||||
});
|
||||
expect(JobTemplatesAPI.readDetail).toHaveBeenCalledWith(7);
|
||||
});
|
||||
test('Displays error fetching full unified job template', async () => {
|
||||
JobTemplatesAPI.readDetail.mockRejectedValueOnce(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/job_templates/7',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(wrapper.find('AlertModal').length).toBe(0);
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-details')
|
||||
.simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal').length).toBe(1);
|
||||
});
|
||||
test('Attempts to fetch credentials on view', async () => {
|
||||
JobTemplatesAPI.readDetail.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 7,
|
||||
name: 'Example',
|
||||
},
|
||||
});
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-details')
|
||||
.simulate('click');
|
||||
});
|
||||
expect(WorkflowJobTemplateNodesAPI.readCredentials).toHaveBeenCalledWith(
|
||||
49
|
||||
);
|
||||
});
|
||||
test('Displays error fetching credentials', async () => {
|
||||
JobTemplatesAPI.readDetail.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 7,
|
||||
name: 'Example',
|
||||
},
|
||||
});
|
||||
WorkflowJobTemplateNodesAPI.readCredentials.mockRejectedValueOnce(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/workflow_job_template_nodes/49/credentials',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(wrapper.find('AlertModal').length).toBe(0);
|
||||
wrapper.find('g').simulate('mouseenter');
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('WorkflowActionTooltipItem#node-details')
|
||||
.simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal').length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import {
|
||||
InventorySourcesAPI,
|
||||
JobTemplatesAPI,
|
||||
ProjectsAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '../../../../api';
|
||||
|
||||
export default function getNodeType(node) {
|
||||
const ujtType = node?.type || node?.unified_job_type;
|
||||
switch (ujtType) {
|
||||
case 'job_template':
|
||||
case 'job':
|
||||
return ['job_template', JobTemplatesAPI];
|
||||
case 'project':
|
||||
case 'project_update':
|
||||
return ['project_sync', ProjectsAPI];
|
||||
case 'inventory_source':
|
||||
case 'inventory_update':
|
||||
return ['inventory_source_sync', InventorySourcesAPI];
|
||||
case 'workflow_job_template':
|
||||
case 'workflow_job':
|
||||
return ['workflow_job_template', WorkflowJobTemplatesAPI];
|
||||
case 'workflow_approval_template':
|
||||
case 'workflow_approval':
|
||||
return ['approval', null];
|
||||
default:
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user