mirror of
https://github.com/ansible/awx.git
synced 2026-05-24 00:57:48 -02:30
Merge pull request #8399 from AlexSCorey/5913-WFNODEPOL
Add WF Node Promptability Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -55,6 +55,19 @@ class WorkflowJobTemplateNodes extends Base {
|
|||||||
readCredentials(id) {
|
readCredentials(id) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
associateCredentials(id, credentialId) {
|
||||||
|
return this.http.post(`${this.baseUrl}${id}/credentials/`, {
|
||||||
|
id: credentialId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disassociateCredentials(id, credentialId) {
|
||||||
|
return this.http.post(`${this.baseUrl}${id}/credentials/`, {
|
||||||
|
id: credentialId,
|
||||||
|
disassociate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WorkflowJobTemplateNodes;
|
export default WorkflowJobTemplateNodes;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class LaunchButton extends React.Component {
|
|||||||
showLaunchPrompt: false,
|
showLaunchPrompt: false,
|
||||||
launchConfig: null,
|
launchConfig: null,
|
||||||
launchError: false,
|
launchError: false,
|
||||||
|
surveyConfig: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleLaunch = this.handleLaunch.bind(this);
|
this.handleLaunch = this.handleLaunch.bind(this);
|
||||||
@@ -67,15 +68,28 @@ class LaunchButton extends React.Component {
|
|||||||
resource.type === 'workflow_job_template'
|
resource.type === 'workflow_job_template'
|
||||||
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
||||||
: JobTemplatesAPI.readLaunch(resource.id);
|
: JobTemplatesAPI.readLaunch(resource.id);
|
||||||
|
const readSurvey =
|
||||||
|
resource.type === 'workflow_job_template'
|
||||||
|
? WorkflowJobTemplatesAPI.readSurvey(resource.id)
|
||||||
|
: JobTemplatesAPI.readSurvey(resource.id);
|
||||||
try {
|
try {
|
||||||
const { data: launchConfig } = await readLaunch;
|
const { data: launchConfig } = await readLaunch;
|
||||||
|
|
||||||
|
let surveyConfig = null;
|
||||||
|
|
||||||
|
if (launchConfig.survey_enabled) {
|
||||||
|
const { data } = await readSurvey;
|
||||||
|
|
||||||
|
surveyConfig = data;
|
||||||
|
}
|
||||||
|
|
||||||
if (canLaunchWithoutPrompt(launchConfig)) {
|
if (canLaunchWithoutPrompt(launchConfig)) {
|
||||||
this.launchWithParams({});
|
this.launchWithParams({});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
showLaunchPrompt: true,
|
showLaunchPrompt: true,
|
||||||
launchConfig,
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -151,7 +165,12 @@ class LaunchButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { launchError, showLaunchPrompt, launchConfig } = this.state;
|
const {
|
||||||
|
launchError,
|
||||||
|
showLaunchPrompt,
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
} = this.state;
|
||||||
const { resource, i18n, children } = this.props;
|
const { resource, i18n, children } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -172,7 +191,8 @@ class LaunchButton extends React.Component {
|
|||||||
)}
|
)}
|
||||||
{showLaunchPrompt && (
|
{showLaunchPrompt && (
|
||||||
<LaunchPrompt
|
<LaunchPrompt
|
||||||
config={launchConfig}
|
launchConfig={launchConfig}
|
||||||
|
surveyConfig={surveyConfig}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
onLaunch={this.launchWithParams}
|
onLaunch={this.launchWithParams}
|
||||||
onCancel={() => this.setState({ showLaunchPrompt: false })}
|
onCancel={() => this.setState({ showLaunchPrompt: false })}
|
||||||
|
|||||||
@@ -6,12 +6,19 @@ import { Formik, useFormikContext } from 'formik';
|
|||||||
import ContentError from '../ContentError';
|
import ContentError from '../ContentError';
|
||||||
import ContentLoading from '../ContentLoading';
|
import ContentLoading from '../ContentLoading';
|
||||||
import { useDismissableError } from '../../util/useRequest';
|
import { useDismissableError } from '../../util/useRequest';
|
||||||
import mergeExtraVars from './mergeExtraVars';
|
import mergeExtraVars from '../../util/prompt/mergeExtraVars';
|
||||||
|
import getSurveyValues from '../../util/prompt/getSurveyValues';
|
||||||
import useLaunchSteps from './useLaunchSteps';
|
import useLaunchSteps from './useLaunchSteps';
|
||||||
import AlertModal from '../AlertModal';
|
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 { values, setTouched, validateForm } = useFormikContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -20,7 +27,7 @@ function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
|||||||
visitStep,
|
visitStep,
|
||||||
visitAllSteps,
|
visitAllSteps,
|
||||||
contentError,
|
contentError,
|
||||||
} = useLaunchSteps(config, resource, i18n);
|
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const postValues = {};
|
const postValues = {};
|
||||||
@@ -39,7 +46,7 @@ function PromptModalForm({ onSubmit, onCancel, i18n, config, resource }) {
|
|||||||
setValue('limit', values.limit);
|
setValue('limit', values.limit);
|
||||||
setValue('job_tags', values.job_tags);
|
setValue('job_tags', values.job_tags);
|
||||||
setValue('skip_tags', values.skip_tags);
|
setValue('skip_tags', values.skip_tags);
|
||||||
const extraVars = config.ask_variables_on_launch
|
const extraVars = launchConfig.ask_variables_on_launch
|
||||||
? values.extra_vars || '---'
|
? values.extra_vars || '---'
|
||||||
: resource.extra_vars;
|
: resource.extra_vars;
|
||||||
setValue('extra_vars', mergeExtraVars(extraVars, surveyValues));
|
setValue('extra_vars', mergeExtraVars(extraVars, surveyValues));
|
||||||
@@ -103,28 +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 (
|
return (
|
||||||
<Formik
|
<Formik initialValues={{}} onSubmit={values => onLaunch(values)}>
|
||||||
initialValues={{
|
|
||||||
verbosity: resource.verbosity || 0,
|
|
||||||
inventory: resource.summary_fields?.inventory || null,
|
|
||||||
credentials: resource.summary_fields?.credentials || null,
|
|
||||||
diff_mode: resource.diff_mode || false,
|
|
||||||
extra_vars: resource.extra_vars || '---',
|
|
||||||
job_type: resource.job_type || '',
|
|
||||||
job_tags: resource.job_tags || '',
|
|
||||||
skip_tags: resource.skip_tags || '',
|
|
||||||
scm_branch: resource.scm_branch || '',
|
|
||||||
limit: resource.limit || '',
|
|
||||||
}}
|
|
||||||
onSubmit={values => onLaunch(values)}
|
|
||||||
>
|
|
||||||
<PromptModalForm
|
<PromptModalForm
|
||||||
onSubmit={values => onLaunch(values)}
|
onSubmit={values => onLaunch(values)}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
config={config}
|
launchConfig={launchConfig}
|
||||||
|
surveyConfig={surveyConfig}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ describe('LaunchPrompt', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<LaunchPrompt
|
<LaunchPrompt
|
||||||
config={{
|
launchConfig={{
|
||||||
...config,
|
...config,
|
||||||
ask_inventory_on_launch: true,
|
ask_inventory_on_launch: true,
|
||||||
ask_credential_on_launch: true,
|
ask_credential_on_launch: true,
|
||||||
@@ -86,6 +86,24 @@ describe('LaunchPrompt', () => {
|
|||||||
resource={resource}
|
resource={resource}
|
||||||
onLaunch={noop}
|
onLaunch={noop}
|
||||||
onCancel={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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -94,10 +112,10 @@ describe('LaunchPrompt', () => {
|
|||||||
|
|
||||||
expect(steps).toHaveLength(5);
|
expect(steps).toHaveLength(5);
|
||||||
expect(steps[0].name.props.children).toEqual('Inventory');
|
expect(steps[0].name.props.children).toEqual('Inventory');
|
||||||
expect(steps[1].name).toEqual('Credentials');
|
expect(steps[1].name.props.children).toEqual('Credentials');
|
||||||
expect(steps[2].name).toEqual('Other Prompts');
|
expect(steps[2].name.props.children).toEqual('Other prompts');
|
||||||
expect(steps[3].name.props.children).toEqual('Survey');
|
expect(steps[3].name.props.children).toEqual('Survey');
|
||||||
expect(steps[4].name).toEqual('Preview');
|
expect(steps[4].name.props.children).toEqual('Preview');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add inventory step', async () => {
|
test('should add inventory step', async () => {
|
||||||
@@ -105,7 +123,7 @@ describe('LaunchPrompt', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<LaunchPrompt
|
<LaunchPrompt
|
||||||
config={{
|
launchConfig={{
|
||||||
...config,
|
...config,
|
||||||
ask_inventory_on_launch: true,
|
ask_inventory_on_launch: true,
|
||||||
}}
|
}}
|
||||||
@@ -129,7 +147,7 @@ describe('LaunchPrompt', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<LaunchPrompt
|
<LaunchPrompt
|
||||||
config={{
|
launchConfig={{
|
||||||
...config,
|
...config,
|
||||||
ask_credential_on_launch: true,
|
ask_credential_on_launch: true,
|
||||||
}}
|
}}
|
||||||
@@ -143,7 +161,7 @@ describe('LaunchPrompt', () => {
|
|||||||
const steps = wizard.prop('steps');
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(2);
|
expect(steps).toHaveLength(2);
|
||||||
expect(steps[0].name).toEqual('Credentials');
|
expect(steps[0].name.props.children).toEqual('Credentials');
|
||||||
expect(isElementOfType(steps[0].component, CredentialsStep)).toEqual(true);
|
expect(isElementOfType(steps[0].component, CredentialsStep)).toEqual(true);
|
||||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||||
});
|
});
|
||||||
@@ -153,7 +171,7 @@ describe('LaunchPrompt', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<LaunchPrompt
|
<LaunchPrompt
|
||||||
config={{
|
launchConfig={{
|
||||||
...config,
|
...config,
|
||||||
ask_verbosity_on_launch: true,
|
ask_verbosity_on_launch: true,
|
||||||
}}
|
}}
|
||||||
@@ -167,7 +185,7 @@ describe('LaunchPrompt', () => {
|
|||||||
const steps = wizard.prop('steps');
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(2);
|
expect(steps).toHaveLength(2);
|
||||||
expect(steps[0].name).toEqual('Other Prompts');
|
expect(steps[0].name.props.children).toEqual('Other prompts');
|
||||||
expect(isElementOfType(steps[0].component, OtherPromptsStep)).toEqual(true);
|
expect(isElementOfType(steps[0].component, OtherPromptsStep)).toEqual(true);
|
||||||
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ const FieldHeader = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function OtherPromptsStep({ config, i18n }) {
|
function OtherPromptsStep({ launchConfig, i18n }) {
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
{config.ask_job_type_on_launch && <JobTypeField i18n={i18n} />}
|
{launchConfig.ask_job_type_on_launch && <JobTypeField i18n={i18n} />}
|
||||||
{config.ask_limit_on_launch && (
|
{launchConfig.ask_limit_on_launch && (
|
||||||
<FormField
|
<FormField
|
||||||
id="prompt-limit"
|
id="prompt-limit"
|
||||||
name="limit"
|
name="limit"
|
||||||
@@ -35,7 +35,7 @@ function OtherPromptsStep({ config, i18n }) {
|
|||||||
information and examples on patterns.`)}
|
information and examples on patterns.`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{config.ask_scm_branch_on_launch && (
|
{launchConfig.ask_scm_branch_on_launch && (
|
||||||
<FormField
|
<FormField
|
||||||
id="prompt-scm-branch"
|
id="prompt-scm-branch"
|
||||||
name="scm_branch"
|
name="scm_branch"
|
||||||
@@ -45,9 +45,11 @@ function OtherPromptsStep({ config, i18n }) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{config.ask_verbosity_on_launch && <VerbosityField i18n={i18n} />}
|
{launchConfig.ask_verbosity_on_launch && <VerbosityField i18n={i18n} />}
|
||||||
{config.ask_diff_mode_on_launch && <ShowChangesToggle i18n={i18n} />}
|
{launchConfig.ask_diff_mode_on_launch && (
|
||||||
{config.ask_tags_on_launch && (
|
<ShowChangesToggle i18n={i18n} />
|
||||||
|
)}
|
||||||
|
{launchConfig.ask_tags_on_launch && (
|
||||||
<TagField
|
<TagField
|
||||||
id="prompt-job-tags"
|
id="prompt-job-tags"
|
||||||
name="job_tags"
|
name="job_tags"
|
||||||
@@ -59,7 +61,7 @@ function OtherPromptsStep({ config, i18n }) {
|
|||||||
documentation for details on the usage of tags.`)}
|
documentation for details on the usage of tags.`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{config.ask_skip_tags_on_launch && (
|
{launchConfig.ask_skip_tags_on_launch && (
|
||||||
<TagField
|
<TagField
|
||||||
id="prompt-skip-tags"
|
id="prompt-skip-tags"
|
||||||
name="skip_tags"
|
name="skip_tags"
|
||||||
@@ -71,7 +73,7 @@ function OtherPromptsStep({ config, i18n }) {
|
|||||||
documentation for details on the usage of tags.`)}
|
documentation for details on the usage of tags.`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{config.ask_variables_on_launch && (
|
{launchConfig.ask_variables_on_launch && (
|
||||||
<VariablesField
|
<VariablesField
|
||||||
id="prompt-variables"
|
id="prompt-variables"
|
||||||
name="extra_vars"
|
name="extra_vars"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe('OtherPromptsStep', () => {
|
|||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Formik initialValues={{ job_type: 'run' }}>
|
<Formik initialValues={{ job_type: 'run' }}>
|
||||||
<OtherPromptsStep
|
<OtherPromptsStep
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_job_type_on_launch: true,
|
ask_job_type_on_launch: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -34,7 +34,7 @@ describe('OtherPromptsStep', () => {
|
|||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Formik>
|
<Formik>
|
||||||
<OtherPromptsStep
|
<OtherPromptsStep
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_limit_on_launch: true,
|
ask_limit_on_launch: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -54,7 +54,7 @@ describe('OtherPromptsStep', () => {
|
|||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Formik>
|
<Formik>
|
||||||
<OtherPromptsStep
|
<OtherPromptsStep
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_scm_branch_on_launch: true,
|
ask_scm_branch_on_launch: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -74,7 +74,7 @@ describe('OtherPromptsStep', () => {
|
|||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Formik initialValues={{ verbosity: '' }}>
|
<Formik initialValues={{ verbosity: '' }}>
|
||||||
<OtherPromptsStep
|
<OtherPromptsStep
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_verbosity_on_launch: true,
|
ask_verbosity_on_launch: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -94,7 +94,7 @@ describe('OtherPromptsStep', () => {
|
|||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Formik initialValues={{ diff_mode: true }}>
|
<Formik initialValues={{ diff_mode: true }}>
|
||||||
<OtherPromptsStep
|
<OtherPromptsStep
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_diff_mode_on_launch: true,
|
ask_diff_mode_on_launch: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { t } from '@lingui/macro';
|
|||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import mergeExtraVars, { maskPasswords } from '../mergeExtraVars';
|
import mergeExtraVars, {
|
||||||
import getSurveyValues from '../getSurveyValues';
|
maskPasswords,
|
||||||
|
} from '../../../util/prompt/mergeExtraVars';
|
||||||
|
import getSurveyValues from '../../../util/prompt/getSurveyValues';
|
||||||
import PromptDetail from '../../PromptDetail';
|
import PromptDetail from '../../PromptDetail';
|
||||||
|
|
||||||
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
||||||
@@ -23,18 +25,25 @@ const ErrorMessageWrapper = styled.div`
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
function PreviewStep({
|
||||||
|
resource,
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
formErrors,
|
||||||
|
i18n,
|
||||||
|
}) {
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
const surveyValues = getSurveyValues(values);
|
const surveyValues = getSurveyValues(values);
|
||||||
|
|
||||||
const overrides = { ...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
|
const initialExtraVars =
|
||||||
? values.extra_vars || '---'
|
launchConfig.ask_variables_on_launch && (overrides.extra_vars || '---');
|
||||||
: resource.extra_vars;
|
if (surveyConfig?.spec) {
|
||||||
if (survey && survey.spec) {
|
const passwordFields = surveyConfig.spec
|
||||||
const passwordFields = survey.spec
|
|
||||||
.filter(q => q.type === 'password')
|
.filter(q => q.type === 'password')
|
||||||
.map(q => q.variable);
|
.map(q => q.variable);
|
||||||
const masked = maskPasswords(surveyValues, passwordFields);
|
const masked = maskPasswords(surveyValues, passwordFields);
|
||||||
@@ -42,7 +51,9 @@ function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
|||||||
mergeExtraVars(initialExtraVars, masked)
|
mergeExtraVars(initialExtraVars, masked)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
overrides.extra_vars = initialExtraVars;
|
overrides.extra_vars = yaml.safeDump(
|
||||||
|
mergeExtraVars(initialExtraVars, {})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +73,7 @@ function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
|||||||
)}
|
)}
|
||||||
<PromptDetail
|
<PromptDetail
|
||||||
resource={resource}
|
resource={resource}
|
||||||
launchConfig={config}
|
launchConfig={launchConfig}
|
||||||
overrides={overrides}
|
overrides={overrides}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ describe('PreviewStep', () => {
|
|||||||
<Formik initialValues={{ limit: '4', survey_foo: 'abc' }}>
|
<Formik initialValues={{ limit: '4', survey_foo: 'abc' }}>
|
||||||
<PreviewStep
|
<PreviewStep
|
||||||
resource={resource}
|
resource={resource}
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_limit_on_launch: true,
|
ask_limit_on_launch: true,
|
||||||
survey_enabled: true,
|
survey_enabled: true,
|
||||||
}}
|
}}
|
||||||
survey={survey}
|
surveyConfig={survey}
|
||||||
formErrors={formErrors}
|
formErrors={formErrors}
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
@@ -64,7 +64,7 @@ describe('PreviewStep', () => {
|
|||||||
<Formik initialValues={{ limit: '4' }}>
|
<Formik initialValues={{ limit: '4' }}>
|
||||||
<PreviewStep
|
<PreviewStep
|
||||||
resource={resource}
|
resource={resource}
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_limit_on_launch: true,
|
ask_limit_on_launch: true,
|
||||||
}}
|
}}
|
||||||
formErrors={formErrors}
|
formErrors={formErrors}
|
||||||
@@ -80,7 +80,32 @@ describe('PreviewStep', () => {
|
|||||||
limit: '4',
|
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 () => {
|
test('should handle extra vars without survey', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -88,7 +113,7 @@ describe('PreviewStep', () => {
|
|||||||
<Formik initialValues={{ extra_vars: 'one: 1' }}>
|
<Formik initialValues={{ extra_vars: 'one: 1' }}>
|
||||||
<PreviewStep
|
<PreviewStep
|
||||||
resource={resource}
|
resource={resource}
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_variables_on_launch: true,
|
ask_variables_on_launch: true,
|
||||||
}}
|
}}
|
||||||
formErrors={formErrors}
|
formErrors={formErrors}
|
||||||
@@ -101,10 +126,9 @@ describe('PreviewStep', () => {
|
|||||||
expect(detail).toHaveLength(1);
|
expect(detail).toHaveLength(1);
|
||||||
expect(detail.prop('resource')).toEqual(resource);
|
expect(detail.prop('resource')).toEqual(resource);
|
||||||
expect(detail.prop('overrides')).toEqual({
|
expect(detail.prop('overrides')).toEqual({
|
||||||
extra_vars: 'one: 1',
|
extra_vars: 'one: 1\n',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should remove survey with empty array value', async () => {
|
test('should remove survey with empty array value', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -115,7 +139,7 @@ describe('PreviewStep', () => {
|
|||||||
>
|
>
|
||||||
<PreviewStep
|
<PreviewStep
|
||||||
resource={resource}
|
resource={resource}
|
||||||
config={{
|
launchConfig={{
|
||||||
ask_variables_on_launch: true,
|
ask_variables_on_launch: true,
|
||||||
}}
|
}}
|
||||||
formErrors={formErrors}
|
formErrors={formErrors}
|
||||||
@@ -128,7 +152,7 @@ describe('PreviewStep', () => {
|
|||||||
expect(detail).toHaveLength(1);
|
expect(detail).toHaveLength(1);
|
||||||
expect(detail.prop('resource')).toEqual(resource);
|
expect(detail.prop('resource')).toEqual(resource);
|
||||||
expect(detail.prop('overrides')).toEqual({
|
expect(detail.prop('overrides')).toEqual({
|
||||||
extra_vars: 'one: 1',
|
extra_vars: 'one: 1\n',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function StepName({ hasErrors, children, i18n }) {
|
function StepName({ hasErrors, children, i18n, id }) {
|
||||||
if (!hasErrors) {
|
if (!hasErrors) {
|
||||||
return children;
|
return <div id={id}>{children}</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AlertText>
|
<AlertText id={id}>
|
||||||
{children}
|
{children}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="right"
|
position="right"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from '../../../util/validators';
|
} from '../../../util/validators';
|
||||||
import { Survey } from '../../../types';
|
import { Survey } from '../../../types';
|
||||||
|
|
||||||
function SurveyStep({ survey, i18n }) {
|
function SurveyStep({ surveyConfig, i18n }) {
|
||||||
const fieldTypes = {
|
const fieldTypes = {
|
||||||
text: TextField,
|
text: TextField,
|
||||||
textarea: TextField,
|
textarea: TextField,
|
||||||
@@ -34,7 +34,7 @@ function SurveyStep({ survey, i18n }) {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
{survey.spec.map(question => {
|
{surveyConfig.spec.map(question => {
|
||||||
const Field = fieldTypes[question.type];
|
const Field = fieldTypes[question.type];
|
||||||
return (
|
return (
|
||||||
<Field key={question.variable} question={question} i18n={i18n} />
|
<Field key={question.variable} question={question} i18n={i18n} />
|
||||||
@@ -44,7 +44,7 @@ function SurveyStep({ survey, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
SurveyStep.propTypes = {
|
SurveyStep.propTypes = {
|
||||||
survey: Survey.isRequired,
|
surveyConfig: Survey.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function TextField({ question, i18n }) {
|
function TextField({ question, i18n }) {
|
||||||
@@ -130,7 +130,8 @@ function MultiSelectField({ question, i18n }) {
|
|||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId={id}
|
fieldId={id}
|
||||||
helperTextInvalid={
|
helperTextInvalid={
|
||||||
meta.error || i18n._(t`Must select a value for this field.`)
|
meta.error ||
|
||||||
|
i18n._(t`At least one value must be selected for this field.`)
|
||||||
}
|
}
|
||||||
isRequired={question.required}
|
isRequired={question.required}
|
||||||
validated={isValid ? 'default' : 'error'}
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import CredentialsStep from './CredentialsStep';
|
import CredentialsStep from './CredentialsStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
const STEP_ID = 'credentials';
|
const STEP_ID = 'credentials';
|
||||||
|
|
||||||
export default function useCredentialsStep(config, i18n) {
|
export default function useCredentialsStep(launchConfig, resource, i18n) {
|
||||||
return {
|
return {
|
||||||
step: getStep(config, i18n),
|
step: getStep(launchConfig, i18n),
|
||||||
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
|
validate: () => ({}),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
formError: null,
|
||||||
@@ -18,13 +21,29 @@ export default function useCredentialsStep(config, i18n) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStep(config, i18n) {
|
function getStep(launchConfig, i18n) {
|
||||||
if (!config.ask_credential_on_launch) {
|
if (!launchConfig.ask_credential_on_launch) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
name: i18n._(t`Credentials`),
|
key: 4,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={false} id="credentials-step">
|
||||||
|
{i18n._(t`Credentials`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
component: <CredentialsStep i18n={i18n} />,
|
component: <CredentialsStep i18n={i18n} />,
|
||||||
|
enableNext: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValues(launchConfig, resource) {
|
||||||
|
if (!launchConfig.ask_credential_on_launch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
credentials: resource?.summary_fields?.credentials || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,22 @@ import StepName from './StepName';
|
|||||||
|
|
||||||
const STEP_ID = 'inventory';
|
const STEP_ID = 'inventory';
|
||||||
|
|
||||||
export default function useInventoryStep(config, visitedSteps, i18n) {
|
export default function useInventoryStep(
|
||||||
|
launchConfig,
|
||||||
|
resource,
|
||||||
|
i18n,
|
||||||
|
visitedSteps
|
||||||
|
) {
|
||||||
const [, meta] = useField('inventory');
|
const [, meta] = useField('inventory');
|
||||||
|
const formError =
|
||||||
|
Object.keys(visitedSteps).includes(STEP_ID) && (!meta.value || meta.error);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(config, meta, i18n, visitedSteps),
|
step: getStep(launchConfig, i18n, formError),
|
||||||
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: !meta.value,
|
formError: launchConfig.ask_inventory_on_launch && formError,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldsTouched => {
|
||||||
setFieldsTouched({
|
setFieldsTouched({
|
||||||
inventory: true,
|
inventory: true,
|
||||||
@@ -21,20 +29,14 @@ export default function useInventoryStep(config, visitedSteps, i18n) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getStep(config, meta, i18n, visitedSteps) {
|
function getStep(launchConfig, i18n, formError) {
|
||||||
if (!config.ask_inventory_on_launch) {
|
if (!launchConfig.ask_inventory_on_launch) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
key: 3,
|
|
||||||
name: (
|
name: (
|
||||||
<StepName
|
<StepName hasErrors={formError} id="inventory-step">
|
||||||
hasErrors={
|
|
||||||
Object.keys(visitedSteps).includes(STEP_ID) &&
|
|
||||||
(!meta.value || meta.error)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{i18n._(t`Inventory`)}
|
{i18n._(t`Inventory`)}
|
||||||
</StepName>
|
</StepName>
|
||||||
),
|
),
|
||||||
@@ -42,3 +44,13 @@ function getStep(config, meta, i18n, visitedSteps) {
|
|||||||
enableNext: true,
|
enableNext: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInitialValues(launchConfig, resource) {
|
||||||
|
if (!launchConfig.ask_inventory_on_launch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inventory: resource?.summary_fields?.inventory || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { jsonToYaml, parseVariableField } from '../../../util/yaml';
|
||||||
import OtherPromptsStep from './OtherPromptsStep';
|
import OtherPromptsStep from './OtherPromptsStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
const STEP_ID = 'other';
|
const STEP_ID = 'other';
|
||||||
|
|
||||||
export default function useOtherPrompt(config, i18n) {
|
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 {
|
return {
|
||||||
step: getStep(config, i18n),
|
step: getStep(launchConfig, i18n),
|
||||||
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
formError: null,
|
||||||
@@ -24,26 +37,66 @@ export default function useOtherPrompt(config, i18n) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStep(config, i18n) {
|
function getStep(launchConfig, i18n) {
|
||||||
if (!shouldShowPrompt(config)) {
|
if (!shouldShowPrompt(launchConfig)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
name: i18n._(t`Other Prompts`),
|
key: 5,
|
||||||
component: <OtherPromptsStep config={config} i18n={i18n} />,
|
name: (
|
||||||
|
<StepName hasErrors={false} id="other-prompts-step">
|
||||||
|
{i18n._(t`Other prompts`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: <OtherPromptsStep launchConfig={launchConfig} i18n={i18n} />,
|
||||||
|
enableNext: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowPrompt(config) {
|
function shouldShowPrompt(launchConfig) {
|
||||||
return (
|
return (
|
||||||
config.ask_job_type_on_launch ||
|
launchConfig.ask_job_type_on_launch ||
|
||||||
config.ask_limit_on_launch ||
|
launchConfig.ask_limit_on_launch ||
|
||||||
config.ask_verbosity_on_launch ||
|
launchConfig.ask_verbosity_on_launch ||
|
||||||
config.ask_tags_on_launch ||
|
launchConfig.ask_tags_on_launch ||
|
||||||
config.ask_skip_tags_on_launch ||
|
launchConfig.ask_skip_tags_on_launch ||
|
||||||
config.ask_variables_on_launch ||
|
launchConfig.ask_variables_on_launch ||
|
||||||
config.ask_scm_branch_on_launch ||
|
launchConfig.ask_scm_branch_on_launch ||
|
||||||
config.ask_diff_mode_on_launch
|
launchConfig.ask_diff_mode_on_launch
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInitialValues(launchConfig, resource) {
|
||||||
|
const initialValues = {};
|
||||||
|
|
||||||
|
if (!launchConfig) {
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchConfig.ask_job_type_on_launch) {
|
||||||
|
initialValues.job_type = resource?.job_type || '';
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_limit_on_launch) {
|
||||||
|
initialValues.limit = resource?.limit || '';
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_verbosity_on_launch) {
|
||||||
|
initialValues.verbosity = resource?.verbosity || 0;
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_tags_on_launch) {
|
||||||
|
initialValues.job_tags = resource?.job_tags || '';
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_skip_tags_on_launch) {
|
||||||
|
initialValues.skip_tags = resource?.skip_tags || '';
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_variables_on_launch) {
|
||||||
|
initialValues.extra_vars = getVariablesData(resource);
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_scm_branch_on_launch) {
|
||||||
|
initialValues.scm_branch = resource?.scm_branch || '';
|
||||||
|
}
|
||||||
|
if (launchConfig.ask_diff_mode_on_launch) {
|
||||||
|
initialValues.diff_mode = resource?.diff_mode || false;
|
||||||
|
}
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,53 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import PreviewStep from './PreviewStep';
|
import PreviewStep from './PreviewStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
const STEP_ID = 'preview';
|
const STEP_ID = 'preview';
|
||||||
|
|
||||||
export default function usePreviewStep(
|
export default function usePreviewStep(
|
||||||
config,
|
launchConfig,
|
||||||
|
i18n,
|
||||||
resource,
|
resource,
|
||||||
survey,
|
surveyConfig,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
i18n
|
showStep
|
||||||
) {
|
) {
|
||||||
const { values: formikValues, errors } = useFormikContext();
|
|
||||||
|
|
||||||
const formErrorsContent = [];
|
|
||||||
if (config.ask_inventory_on_launch && !formikValues.inventory) {
|
|
||||||
formErrorsContent.push({
|
|
||||||
inventory: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const hasSurveyError = Object.keys(errors).find(e => e.includes('survey'));
|
|
||||||
if (
|
|
||||||
config.survey_enabled &&
|
|
||||||
(config.variables_needed_to_start ||
|
|
||||||
config.variables_needed_to_start.length === 0) &&
|
|
||||||
hasSurveyError
|
|
||||||
) {
|
|
||||||
formErrorsContent.push({
|
|
||||||
survey: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: {
|
step: showStep
|
||||||
|
? {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
name: i18n._(t`Preview`),
|
name: (
|
||||||
|
<StepName hasErrors={false} id="preview-step">
|
||||||
|
{i18n._(t`Preview`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
component: (
|
component: (
|
||||||
<PreviewStep
|
<PreviewStep
|
||||||
config={config}
|
launchConfig={launchConfig}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
survey={survey}
|
surveyConfig={surveyConfig}
|
||||||
formErrors={hasErrors}
|
formErrors={hasErrors}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: !hasErrors,
|
enableNext: !hasErrors,
|
||||||
nextButtonText: i18n._(t`Launch`),
|
nextButtonText: i18n._(t`Launch`),
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
initialValues: {},
|
initialValues: {},
|
||||||
|
validate: () => ({}),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
error: null,
|
error: null,
|
||||||
setTouched: () => {},
|
setTouched: () => {},
|
||||||
|
|||||||
@@ -1,39 +1,25 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import useRequest from '../../../util/useRequest';
|
|
||||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../api';
|
|
||||||
import SurveyStep from './SurveyStep';
|
import SurveyStep from './SurveyStep';
|
||||||
import StepName from './StepName';
|
import StepName from './StepName';
|
||||||
|
|
||||||
const STEP_ID = 'survey';
|
const STEP_ID = 'survey';
|
||||||
|
|
||||||
export default function useSurveyStep(config, visitedSteps, i18n) {
|
export default function useSurveyStep(
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
resource,
|
||||||
|
i18n,
|
||||||
|
visitedSteps
|
||||||
|
) {
|
||||||
const { values } = useFormikContext();
|
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 validate = () => {
|
|
||||||
if (!config.survey_enabled || !survey || !survey.spec) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const errors = {};
|
const errors = {};
|
||||||
survey.spec.forEach(question => {
|
const validate = () => {
|
||||||
|
if (!launchConfig.survey_enabled || !surveyConfig?.spec) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
surveyConfig.spec.forEach(question => {
|
||||||
const errMessage = validateField(
|
const errMessage = validateField(
|
||||||
question,
|
question,
|
||||||
values[`survey_${question.variable}`],
|
values[`survey_${question.variable}`],
|
||||||
@@ -47,18 +33,19 @@ export default function useSurveyStep(config, visitedSteps, i18n) {
|
|||||||
};
|
};
|
||||||
const formError = Object.keys(validate()).length > 0;
|
const formError = Object.keys(validate()).length > 0;
|
||||||
return {
|
return {
|
||||||
step: getStep(config, survey, formError, i18n, visitedSteps),
|
step: getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps),
|
||||||
|
initialValues: getInitialValues(launchConfig, surveyConfig, resource),
|
||||||
|
validate,
|
||||||
|
surveyConfig,
|
||||||
|
isReady: true,
|
||||||
|
contentError: null,
|
||||||
formError,
|
formError,
|
||||||
initialValues: getInitialValues(config, survey),
|
|
||||||
survey,
|
|
||||||
isReady: !isLoading && !!survey,
|
|
||||||
contentError: error,
|
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldsTouched => {
|
||||||
if (!survey || !survey.spec) {
|
if (!surveyConfig?.spec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fields = {};
|
const fields = {};
|
||||||
survey.spec.forEach(question => {
|
surveyConfig.spec.forEach(question => {
|
||||||
fields[`survey_${question.variable}`] = true;
|
fields[`survey_${question.variable}`] = true;
|
||||||
});
|
});
|
||||||
setFieldsTouched(fields);
|
setFieldsTouched(fields);
|
||||||
@@ -84,49 +71,65 @@ function validateField(question, value, i18n) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (question.required && !value && value !== 0) {
|
||||||
question.required &&
|
|
||||||
((!value && value !== 0) || (Array.isArray(value) && value.length === 0))
|
|
||||||
) {
|
|
||||||
return i18n._(t`This field must not be blank`);
|
return i18n._(t`This field must not be blank`);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function getStep(config, survey, hasErrors, i18n, visitedSteps) {
|
function getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps) {
|
||||||
if (!config.survey_enabled) {
|
if (!launchConfig.survey_enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: STEP_ID,
|
id: STEP_ID,
|
||||||
key: 6,
|
|
||||||
name: (
|
name: (
|
||||||
<StepName
|
<StepName
|
||||||
hasErrors={Object.keys(visitedSteps).includes(STEP_ID) && hasErrors}
|
hasErrors={
|
||||||
|
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
|
Object.keys(validate()).length
|
||||||
|
}
|
||||||
|
id="survey-step"
|
||||||
>
|
>
|
||||||
{i18n._(t`Survey`)}
|
{i18n._(t`Survey`)}
|
||||||
</StepName>
|
</StepName>
|
||||||
),
|
),
|
||||||
component: <SurveyStep survey={survey} i18n={i18n} />,
|
component: <SurveyStep surveyConfig={surveyConfig} i18n={i18n} />,
|
||||||
enableNext: true,
|
enableNext: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getInitialValues(config, survey) {
|
|
||||||
if (!config.survey_enabled || !survey) {
|
function getInitialValues(launchConfig, surveyConfig, resource) {
|
||||||
|
if (!launchConfig.survey_enabled || !surveyConfig) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const surveyValues = {};
|
|
||||||
survey.spec.forEach(question => {
|
const values = {};
|
||||||
|
if (surveyConfig?.spec) {
|
||||||
|
surveyConfig.spec.forEach(question => {
|
||||||
if (question.type === 'multiselect') {
|
if (question.type === 'multiselect') {
|
||||||
if (question.default === '') {
|
values[`survey_${question.variable}`] = question.default
|
||||||
surveyValues[`survey_${question.variable}`] = [];
|
? question.default.split('\n')
|
||||||
|
: [];
|
||||||
|
} else if (question.type === 'multiplechoice') {
|
||||||
|
values[`survey_${question.variable}`] =
|
||||||
|
question.default || question.choices.split('\n')[0];
|
||||||
} else {
|
} else {
|
||||||
surveyValues[`survey_${question.variable}`] = question.default.split(
|
values[`survey_${question.variable}`] = question.default || '';
|
||||||
'\n'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
} else {
|
} else {
|
||||||
surveyValues[`survey_${question.variable}`] = question.default;
|
values[`survey_${question.variable}`] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return surveyValues;
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,48 @@ import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
|||||||
import useSurveyStep from './steps/useSurveyStep';
|
import useSurveyStep from './steps/useSurveyStep';
|
||||||
import usePreviewStep from './steps/usePreviewStep';
|
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 [visited, setVisited] = useState({});
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
const steps = [
|
const steps = [
|
||||||
useInventoryStep(config, visited, i18n),
|
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||||
useCredentialsStep(config, i18n),
|
useCredentialsStep(launchConfig, resource, i18n),
|
||||||
useOtherPromptsStep(config, i18n),
|
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||||
useSurveyStep(config, visited, i18n),
|
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||||
];
|
];
|
||||||
const { resetForm, values: formikValues } = useFormikContext();
|
const { resetForm } = useFormikContext();
|
||||||
const hasErrors = steps.some(step => step.formError);
|
const hasErrors = steps.some(step => step.formError);
|
||||||
|
|
||||||
const surveyStepIndex = steps.findIndex(step => step.survey);
|
|
||||||
steps.push(
|
steps.push(
|
||||||
usePreviewStep(
|
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
||||||
config,
|
|
||||||
resource,
|
|
||||||
steps[surveyStepIndex]?.survey,
|
|
||||||
hasErrors,
|
|
||||||
i18n
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (surveyStepIndex > -1 && isReady) {
|
if (stepsAreReady) {
|
||||||
|
const initialValues = steps.reduce((acc, cur) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
...cur.initialValues,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
resetForm({
|
resetForm({
|
||||||
values: {
|
values: {
|
||||||
...formikValues,
|
...initialValues,
|
||||||
...steps[surveyStepIndex].initialValues,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setIsReady(true);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isReady]);
|
}, [stepsAreReady]);
|
||||||
|
|
||||||
const stepWithError = steps.find(s => s.contentError);
|
const stepWithError = steps.find(s => s.contentError);
|
||||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function buildResourceLink(resource) {
|
|||||||
|
|
||||||
function hasPromptData(launchData) {
|
function hasPromptData(launchData) {
|
||||||
return (
|
return (
|
||||||
|
launchData.survey_enabled ||
|
||||||
launchData.ask_credential_on_launch ||
|
launchData.ask_credential_on_launch ||
|
||||||
launchData.ask_diff_mode_on_launch ||
|
launchData.ask_diff_mode_on_launch ||
|
||||||
launchData.ask_inventory_on_launch ||
|
launchData.ask_inventory_on_launch ||
|
||||||
@@ -66,14 +67,15 @@ function hasPromptData(launchData) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function omitOverrides(resource, overrides) {
|
function omitOverrides(resource, overrides, defaultConfig) {
|
||||||
const clonedResource = {
|
const clonedResource = {
|
||||||
...resource,
|
...resource,
|
||||||
summary_fields: { ...resource.summary_fields },
|
summary_fields: { ...resource.summary_fields },
|
||||||
|
...defaultConfig,
|
||||||
};
|
};
|
||||||
Object.keys(overrides).forEach(keyToOmit => {
|
Object.keys(overrides).forEach(keyToOmit => {
|
||||||
delete clonedResource[keyToOmit];
|
delete clonedResource[keyToOmit];
|
||||||
delete clonedResource.summary_fields[keyToOmit];
|
delete clonedResource?.summary_fields[keyToOmit];
|
||||||
});
|
});
|
||||||
return clonedResource;
|
return clonedResource;
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,8 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
|||||||
4: i18n._(t`4 (Connection Debug)`),
|
4: i18n._(t`4 (Connection Debug)`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const details = omitOverrides(resource, overrides);
|
const details = omitOverrides(resource, overrides, launchConfig.defaults);
|
||||||
|
details.type = overrides?.nodeType || details.type;
|
||||||
const hasOverrides = Object.keys(overrides).length > 0;
|
const hasOverrides = Object.keys(overrides).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -136,13 +139,13 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
|||||||
<Divider css="margin-top: var(--pf-global--spacer--lg)" />
|
<Divider css="margin-top: var(--pf-global--spacer--lg)" />
|
||||||
<PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader>
|
<PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader>
|
||||||
<DetailList aria-label="Prompt Overrides">
|
<DetailList aria-label="Prompt Overrides">
|
||||||
{overrides?.job_type && (
|
{launchConfig.ask_job_type_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Job Type`)}
|
label={i18n._(t`Job Type`)}
|
||||||
value={toTitleCase(overrides.job_type)}
|
value={toTitleCase(overrides.job_type)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.credentials && (
|
{launchConfig.ask_credential_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={i18n._(t`Credentials`)}
|
label={i18n._(t`Credentials`)}
|
||||||
@@ -163,37 +166,43 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.inventory && (
|
{launchConfig.ask_inventory_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Inventory`)}
|
label={i18n._(t`Inventory`)}
|
||||||
value={overrides.inventory?.name}
|
value={overrides.inventory?.name}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.scm_branch && (
|
{launchConfig.ask_scm_branch_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Source Control Branch`)}
|
label={i18n._(t`Source Control Branch`)}
|
||||||
value={overrides.scm_branch}
|
value={overrides.scm_branch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.limit && (
|
{launchConfig.ask_limit_on_launch && (
|
||||||
<Detail label={i18n._(t`Limit`)} value={overrides.limit} />
|
<Detail label={i18n._(t`Limit`)} value={overrides.limit} />
|
||||||
)}
|
)}
|
||||||
{overrides?.verbosity && (
|
{Object.prototype.hasOwnProperty.call(overrides, 'verbosity') &&
|
||||||
|
launchConfig.ask_verbosity_on_launch ? (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Verbosity`)}
|
label={i18n._(t`Verbosity`)}
|
||||||
value={VERBOSITY[overrides.verbosity]}
|
value={VERBOSITY[overrides.verbosity]}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
{overrides?.job_tags && (
|
{launchConfig.ask_tags_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={i18n._(t`Job Tags`)}
|
label={i18n._(t`Job Tags`)}
|
||||||
value={
|
value={
|
||||||
<ChipGroup
|
<ChipGroup
|
||||||
numChips={5}
|
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 => (
|
{overrides.job_tags.length > 0 &&
|
||||||
|
overrides.job_tags.split(',').map(jobTag => (
|
||||||
<Chip key={jobTag} isReadOnly>
|
<Chip key={jobTag} isReadOnly>
|
||||||
{jobTag}
|
{jobTag}
|
||||||
</Chip>
|
</Chip>
|
||||||
@@ -202,16 +211,21 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.skip_tags && (
|
{launchConfig.ask_skip_tags_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={i18n._(t`Skip Tags`)}
|
label={i18n._(t`Skip Tags`)}
|
||||||
value={
|
value={
|
||||||
<ChipGroup
|
<ChipGroup
|
||||||
numChips={5}
|
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 => (
|
{overrides.skip_tags.length > 0 &&
|
||||||
|
overrides.skip_tags.split(',').map(skipTag => (
|
||||||
<Chip key={skipTag} isReadOnly>
|
<Chip key={skipTag} isReadOnly>
|
||||||
{skipTag}
|
{skipTag}
|
||||||
</Chip>
|
</Chip>
|
||||||
@@ -220,7 +234,7 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.diff_mode && (
|
{launchConfig.ask_diff_mode_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Show Changes`)}
|
label={i18n._(t`Show Changes`)}
|
||||||
value={
|
value={
|
||||||
@@ -228,7 +242,8 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{overrides?.extra_vars && (
|
{(launchConfig.survey_enabled ||
|
||||||
|
launchConfig.ask_variables_on_launch) && (
|
||||||
<VariablesDetail
|
<VariablesDetail
|
||||||
label={i18n._(t`Variables`)}
|
label={i18n._(t`Variables`)}
|
||||||
rows={4}
|
rows={4}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const mockPromptLaunch = {
|
|||||||
defaults: {
|
defaults: {
|
||||||
extra_vars: '---foo: bar',
|
extra_vars: '---foo: bar',
|
||||||
diff_mode: false,
|
diff_mode: false,
|
||||||
limit: 3,
|
limit: 'localhost',
|
||||||
job_tags: 'T_100,T_200',
|
job_tags: 'T_100,T_200',
|
||||||
skip_tags: 'S_100,S_200',
|
skip_tags: 'S_100,S_200',
|
||||||
job_type: 'run',
|
job_type: 'run',
|
||||||
@@ -74,7 +74,7 @@ describe('PromptDetail', () => {
|
|||||||
assertDetail('Job Type', 'Run');
|
assertDetail('Job Type', 'Run');
|
||||||
assertDetail('Inventory', 'Demo Inventory');
|
assertDetail('Inventory', 'Demo Inventory');
|
||||||
assertDetail('Source Control Branch', 'Foo branch');
|
assertDetail('Source Control Branch', 'Foo branch');
|
||||||
assertDetail('Limit', 'alpha:beta');
|
assertDetail('Limit', 'localhost');
|
||||||
assertDetail('Verbosity', '3 (Debug)');
|
assertDetail('Verbosity', '3 (Debug)');
|
||||||
assertDetail('Show Changes', 'Off');
|
assertDetail('Show Changes', 'Off');
|
||||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||||
@@ -151,6 +151,14 @@ describe('PromptDetail', () => {
|
|||||||
inventory: {
|
inventory: {
|
||||||
name: 'Override 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(() => {
|
beforeAll(() => {
|
||||||
@@ -180,12 +188,12 @@ describe('PromptDetail', () => {
|
|||||||
assertDetail('Name', 'Mock JT');
|
assertDetail('Name', 'Mock JT');
|
||||||
assertDetail('Description', 'Mock JT Description');
|
assertDetail('Description', 'Mock JT Description');
|
||||||
assertDetail('Type', 'Job Template');
|
assertDetail('Type', 'Job Template');
|
||||||
assertDetail('Job Type', 'Run');
|
assertDetail('Job Type', 'Check');
|
||||||
assertDetail('Inventory', 'Override inventory');
|
assertDetail('Inventory', 'Override inventory');
|
||||||
assertDetail('Source Control Branch', 'Foo branch');
|
assertDetail('Source Control Branch', 'Bar branch');
|
||||||
assertDetail('Limit', 'alpha:beta');
|
assertDetail('Limit', 'otherlimit');
|
||||||
assertDetail('Verbosity', '3 (Debug)');
|
assertDetail('Verbosity', '0 (Normal)');
|
||||||
assertDetail('Show Changes', 'Off');
|
assertDetail('Show Changes', 'On');
|
||||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||||
'---one: two\nbar: baz'
|
'---one: two\nbar: baz'
|
||||||
);
|
);
|
||||||
@@ -204,12 +212,12 @@ describe('PromptDetail', () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Job Tags"]')
|
.find('Detail[label="Job Tags"]')
|
||||||
.containsAnyMatchingElements([<span>T_100</span>, <span>T_200</span>])
|
.containsAnyMatchingElements([<span>foo</span>, <span>bar</span>])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Skip Tags"]')
|
.find('Detail[label="Skip Tags"]')
|
||||||
.containsAllMatchingElements([<span>S_100</span>, <span>S_200</span>])
|
.containsAllMatchingElements([<span>baz</span>, <span>boo</span>])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
|
|||||||
? 'smart_inventory'
|
? 'smart_inventory'
|
||||||
: 'inventory';
|
: 'inventory';
|
||||||
|
|
||||||
const recentJobs = summary_fields.recent_jobs.map(job => ({
|
const recentJobs = summary_fields?.recent_jobs?.map(job => ({
|
||||||
...job,
|
...job,
|
||||||
type: 'job',
|
type: 'job',
|
||||||
}));
|
}));
|
||||||
@@ -133,10 +133,12 @@ function PromptJobTemplateDetail({ i18n, resource }) {
|
|||||||
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />
|
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />
|
||||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
<Detail label={i18n._(t`Limit`)} value={limit} />
|
||||||
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
|
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
|
||||||
|
{typeof diff_mode === 'boolean' && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Show Changes`)}
|
label={i18n._(t`Show Changes`)}
|
||||||
value={diff_mode ? i18n._(t`On`) : i18n._(t`Off`)}
|
value={diff_mode ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
|
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
|
||||||
<Detail label={i18n._(t`Host Config Key`)} value={host_config_key} />
|
<Detail label={i18n._(t`Host Config Key`)} value={host_config_key} />
|
||||||
{related?.callback && (
|
{related?.callback && (
|
||||||
@@ -149,7 +151,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
|
|||||||
label={i18n._(t`Webhook Service`)}
|
label={i18n._(t`Webhook Service`)}
|
||||||
value={toTitleCase(webhook_service)}
|
value={toTitleCase(webhook_service)}
|
||||||
/>
|
/>
|
||||||
{related.webhook_receiver && (
|
{related?.webhook_receiver && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Webhook URL`)}
|
label={i18n._(t`Webhook URL`)}
|
||||||
value={`${window.location.origin}${related.webhook_receiver}`}
|
value={`${window.location.origin}${related.webhook_receiver}`}
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ function PromptWFJobTemplateDetail({ i18n, resource }) {
|
|||||||
? 'smart_inventory'
|
? 'smart_inventory'
|
||||||
: 'inventory';
|
: 'inventory';
|
||||||
|
|
||||||
const recentJobs = summary_fields.recent_jobs.map(job => ({
|
const recentJobs = summary_fields?.recent_jobs?.map(job => ({
|
||||||
...job,
|
...job,
|
||||||
type: 'job',
|
type: 'job',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{summary_fields.recent_jobs?.length > 0 && (
|
{summary_fields?.recent_jobs?.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
value={<Sparkline jobs={recentJobs} />}
|
value={<Sparkline jobs={recentJobs} />}
|
||||||
label={i18n._(t`Activity`)}
|
label={i18n._(t`Activity`)}
|
||||||
@@ -84,7 +84,7 @@ function PromptWFJobTemplateDetail({ i18n, resource }) {
|
|||||||
value={toTitleCase(webhook_service)}
|
value={toTitleCase(webhook_service)}
|
||||||
/>
|
/>
|
||||||
<Detail label={i18n._(t`Webhook Key`)} value={webhook_key} />
|
<Detail label={i18n._(t`Webhook Key`)} value={webhook_key} />
|
||||||
{related.webhook_receiver && (
|
{related?.webhook_receiver && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Webhook URL`)}
|
label={i18n._(t`Webhook URL`)}
|
||||||
value={`${window.location.origin}${related.webhook_receiver}`}
|
value={`${window.location.origin}${related.webhook_receiver}`}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const Link = styled(props => <_Link {...props} />)`
|
|||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
`;
|
`;
|
||||||
/* eslint-enable react/jsx-pascal-case */
|
/* eslint-enable react/jsx-pascal-case */
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)`
|
|||||||
function WorkflowNodeHelp({ node, i18n }) {
|
function WorkflowNodeHelp({ node, i18n }) {
|
||||||
let nodeType;
|
let nodeType;
|
||||||
const job = node?.originalNodeObject?.summary_fields?.job;
|
const job = node?.originalNodeObject?.summary_fields?.job;
|
||||||
if (node.unifiedJobTemplate || job) {
|
const unifiedJobTemplate =
|
||||||
const type = node.unifiedJobTemplate
|
node?.fullUnifiedJobTemplate ||
|
||||||
? node.unifiedJobTemplate.unified_job_type || node.unifiedJobTemplate.type
|
node?.originalNodeObject?.summary_fields?.unified_job_template;
|
||||||
|
if (unifiedJobTemplate || job) {
|
||||||
|
const type = unifiedJobTemplate
|
||||||
|
? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type
|
||||||
: job.type;
|
: job.type;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'job_template':
|
case 'job_template':
|
||||||
@@ -113,7 +116,7 @@ function WorkflowNodeHelp({ node, i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!node.unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && (
|
{!unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && (
|
||||||
<>
|
<>
|
||||||
<ResourceDeleted job={job}>
|
<ResourceDeleted job={job}>
|
||||||
<StyledExclamationTriangleIcon />
|
<StyledExclamationTriangleIcon />
|
||||||
@@ -149,12 +152,12 @@ function WorkflowNodeHelp({ node, i18n }) {
|
|||||||
)}
|
)}
|
||||||
</GridDL>
|
</GridDL>
|
||||||
)}
|
)}
|
||||||
{node.unifiedJobTemplate && !job && (
|
{unifiedJobTemplate && !job && (
|
||||||
<GridDL>
|
<GridDL>
|
||||||
<dt>
|
<dt>
|
||||||
<b>{i18n._(t`Name`)}</b>
|
<b>{i18n._(t`Name`)}</b>
|
||||||
</dt>
|
</dt>
|
||||||
<dd id="workflow-node-help-name">{node.unifiedJobTemplate.name}</dd>
|
<dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd>
|
||||||
<dt>
|
<dt>
|
||||||
<b>{i18n._(t`Type`)}</b>
|
<b>{i18n._(t`Type`)}</b>
|
||||||
</dt>
|
</dt>
|
||||||
|
|||||||
@@ -19,16 +19,27 @@ const CenteredPauseIcon = styled(PauseIcon)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowNodeTypeLetter({ node }) {
|
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;
|
let nodeTypeLetter;
|
||||||
if (
|
if (
|
||||||
(node.unifiedJobTemplate &&
|
unifiedJobTemplate.type ||
|
||||||
(node.unifiedJobTemplate.type ||
|
unifiedJobTemplate.unified_job_type ||
|
||||||
node.unifiedJobTemplate.unified_job_type)) ||
|
node?.job?.type
|
||||||
(node.job && node.job.type)
|
|
||||||
) {
|
) {
|
||||||
const ujtType = node.unifiedJobTemplate
|
const ujtType =
|
||||||
? node.unifiedJobTemplate.type || node.unifiedJobTemplate.unified_job_type
|
unifiedJobTemplate.type ||
|
||||||
: node.job.type;
|
unifiedJobTemplate.unified_job_type ||
|
||||||
|
node.job.type;
|
||||||
switch (ujtType) {
|
switch (ujtType) {
|
||||||
case 'job_template':
|
case 'job_template':
|
||||||
case 'job':
|
case 'job':
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { type: 'job_template' } }}
|
node={{ fullUnifiedJobTemplate: { type: 'job_template' } }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -19,7 +19,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { unified_job_type: 'job' } }}
|
node={{ fullUnifiedJobTemplate: { unified_job_type: 'job' } }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -30,7 +30,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { type: 'project' } }}
|
node={{ fullUnifiedJobTemplate: { type: 'project' } }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,9 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { unified_job_type: 'project_update' } }}
|
node={{
|
||||||
|
fullUnifiedJobTemplate: { unified_job_type: 'project_update' },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -52,7 +54,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { type: 'inventory_source' } }}
|
node={{ fullUnifiedJobTemplate: { type: 'inventory_source' } }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -64,7 +66,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{
|
node={{
|
||||||
unifiedJobTemplate: { unified_job_type: 'inventory_update' },
|
fullUnifiedJobTemplate: { unified_job_type: 'inventory_update' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -76,7 +78,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { type: 'workflow_job_template' } }}
|
node={{ fullUnifiedJobTemplate: { type: 'workflow_job_template' } }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -87,7 +89,9 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { unified_job_type: 'workflow_job' } }}
|
node={{
|
||||||
|
fullUnifiedJobTemplate: { unified_job_type: 'workflow_job' },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -98,7 +102,9 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{ unifiedJobTemplate: { type: 'workflow_approval_template' } }}
|
node={{
|
||||||
|
fullUnifiedJobTemplate: { type: 'workflow_approval_template' },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -110,7 +116,7 @@ describe('WorkflowNodeTypeLetter', () => {
|
|||||||
<svg>
|
<svg>
|
||||||
<WorkflowNodeTypeLetter
|
<WorkflowNodeTypeLetter
|
||||||
node={{
|
node={{
|
||||||
unifiedJobTemplate: { unified_job_type: 'workflow_approval' },
|
fullUnifiedJobTemplate: { unified_job_type: 'workflow_approval' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -55,27 +55,60 @@ export default function visualizerReducer(state, action) {
|
|||||||
case 'SELECT_SOURCE_FOR_LINKING':
|
case 'SELECT_SOURCE_FOR_LINKING':
|
||||||
return selectSourceForLinking(state, action.node);
|
return selectSourceForLinking(state, action.node);
|
||||||
case 'SET_ADD_LINK_TARGET_NODE':
|
case 'SET_ADD_LINK_TARGET_NODE':
|
||||||
return { ...state, addLinkTargetNode: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
addLinkTargetNode: action.value,
|
||||||
|
};
|
||||||
case 'SET_CONTENT_ERROR':
|
case 'SET_CONTENT_ERROR':
|
||||||
return { ...state, contentError: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
contentError: action.value,
|
||||||
|
};
|
||||||
case 'SET_IS_LOADING':
|
case 'SET_IS_LOADING':
|
||||||
return { ...state, isLoading: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: action.value,
|
||||||
|
};
|
||||||
case 'SET_LINK_TO_DELETE':
|
case 'SET_LINK_TO_DELETE':
|
||||||
return { ...state, linkToDelete: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
linkToDelete: action.value,
|
||||||
|
};
|
||||||
case 'SET_LINK_TO_EDIT':
|
case 'SET_LINK_TO_EDIT':
|
||||||
return { ...state, linkToEdit: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
linkToEdit: action.value,
|
||||||
|
};
|
||||||
case 'SET_NODES':
|
case 'SET_NODES':
|
||||||
return { ...state, nodes: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: action.value,
|
||||||
|
};
|
||||||
case 'SET_NODE_POSITIONS':
|
case 'SET_NODE_POSITIONS':
|
||||||
return { ...state, nodePositions: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
nodePositions: action.value,
|
||||||
|
};
|
||||||
case 'SET_NODE_TO_DELETE':
|
case 'SET_NODE_TO_DELETE':
|
||||||
return { ...state, nodeToDelete: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
nodeToDelete: action.value,
|
||||||
|
};
|
||||||
case 'SET_NODE_TO_EDIT':
|
case 'SET_NODE_TO_EDIT':
|
||||||
return { ...state, nodeToEdit: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
nodeToEdit: action.value,
|
||||||
|
};
|
||||||
case 'SET_NODE_TO_VIEW':
|
case 'SET_NODE_TO_VIEW':
|
||||||
return { ...state, nodeToView: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
nodeToView: action.value,
|
||||||
|
};
|
||||||
case 'SET_SHOW_DELETE_ALL_NODES_MODAL':
|
case 'SET_SHOW_DELETE_ALL_NODES_MODAL':
|
||||||
return { ...state, showDeleteAllNodesModal: action.value };
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteAllNodesModal: action.value,
|
||||||
|
};
|
||||||
case 'START_ADD_NODE':
|
case 'START_ADD_NODE':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -113,8 +146,12 @@ function createLink(state, linkType) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: addLinkSourceNode.id },
|
source: {
|
||||||
target: { id: addLinkTargetNode.id },
|
id: addLinkSourceNode.id,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: addLinkTargetNode.id,
|
||||||
|
},
|
||||||
linkType,
|
linkType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,8 +180,9 @@ function createNode(state, node) {
|
|||||||
|
|
||||||
newNodes.push({
|
newNodes.push({
|
||||||
id: nextNodeId,
|
id: nextNodeId,
|
||||||
unifiedJobTemplate: node.nodeResource,
|
fullUnifiedJobTemplate: node.nodeResource,
|
||||||
isInvalidLinkTarget: false,
|
isInvalidLinkTarget: false,
|
||||||
|
promptValues: node.promptValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensures that root nodes appear to always run
|
// Ensures that root nodes appear to always run
|
||||||
@@ -154,8 +192,12 @@ function createNode(state, node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: addNodeSource },
|
source: {
|
||||||
target: { id: nextNodeId },
|
id: addNodeSource,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: nextNodeId,
|
||||||
|
},
|
||||||
linkType: node.linkType,
|
linkType: node.linkType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,7 +207,9 @@ function createNode(state, node) {
|
|||||||
linkToCompare.source.id === addNodeSource &&
|
linkToCompare.source.id === addNodeSource &&
|
||||||
linkToCompare.target.id === addNodeTarget
|
linkToCompare.target.id === addNodeTarget
|
||||||
) {
|
) {
|
||||||
linkToCompare.source = { id: nextNodeId };
|
linkToCompare.source = {
|
||||||
|
id: nextNodeId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -268,15 +312,23 @@ function addLinksFromParentsToChildren(
|
|||||||
// doesn't have any other parents
|
// doesn't have any other parents
|
||||||
if (linkParentMapping[child.id].length === 1) {
|
if (linkParentMapping[child.id].length === 1) {
|
||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: parentId },
|
source: {
|
||||||
target: { id: child.id },
|
id: parentId,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: child.id,
|
||||||
|
},
|
||||||
linkType: 'always',
|
linkType: 'always',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (!linkParentMapping[child.id].includes(parentId)) {
|
} else if (!linkParentMapping[child.id].includes(parentId)) {
|
||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: parentId },
|
source: {
|
||||||
target: { id: child.id },
|
id: parentId,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: child.id,
|
||||||
|
},
|
||||||
linkType: child.linkType,
|
linkType: child.linkType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -302,7 +354,10 @@ function removeLinksFromDeletedNode(
|
|||||||
|
|
||||||
if (link.source.id === nodeId || link.target.id === nodeId) {
|
if (link.source.id === nodeId || link.target.id === nodeId) {
|
||||||
if (link.source.id === nodeId) {
|
if (link.source.id === nodeId) {
|
||||||
children.push({ id: link.target.id, linkType: link.linkType });
|
children.push({
|
||||||
|
id: link.target.id,
|
||||||
|
linkType: link.linkType,
|
||||||
|
});
|
||||||
} else if (link.target.id === nodeId) {
|
} else if (link.target.id === nodeId) {
|
||||||
parents.push(link.source.id);
|
parents.push(link.source.id);
|
||||||
}
|
}
|
||||||
@@ -352,7 +407,7 @@ function generateNodes(workflowNodes, i18n) {
|
|||||||
const arrayOfNodesForChart = [
|
const arrayOfNodesForChart = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
name: i18n._(t`START`),
|
name: i18n._(t`START`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,8 +420,14 @@ function generateNodes(workflowNodes, i18n) {
|
|||||||
originalNodeObject: node,
|
originalNodeObject: node,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (node.summary_fields.unified_job_template) {
|
if (
|
||||||
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
|
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);
|
arrayOfNodesForChart.push(nodeObj);
|
||||||
@@ -596,11 +657,19 @@ function updateLink(state, linkType) {
|
|||||||
|
|
||||||
function updateNode(state, editedNode) {
|
function updateNode(state, editedNode) {
|
||||||
const { nodeToEdit, nodes } = state;
|
const { nodeToEdit, nodes } = state;
|
||||||
|
const { nodeResource, launchConfig, promptValues } = editedNode;
|
||||||
const newNodes = [...nodes];
|
const newNodes = [...nodes];
|
||||||
|
|
||||||
const matchingNode = newNodes.find(node => node.id === nodeToEdit.id);
|
const matchingNode = newNodes.find(node => node.id === nodeToEdit.id);
|
||||||
matchingNode.unifiedJobTemplate = editedNode.nodeResource;
|
matchingNode.fullUnifiedJobTemplate = nodeResource;
|
||||||
matchingNode.isEdited = true;
|
matchingNode.isEdited = true;
|
||||||
|
matchingNode.launchConfig = launchConfig;
|
||||||
|
|
||||||
|
if (promptValues) {
|
||||||
|
matchingNode.promptValues = promptValues;
|
||||||
|
} else {
|
||||||
|
delete matchingNode.promptValues;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -615,7 +684,15 @@ function refreshNode(state, refreshedNode) {
|
|||||||
const newNodes = [...nodes];
|
const newNodes = [...nodes];
|
||||||
|
|
||||||
const matchingNode = newNodes.find(node => node.id === nodeToView.id);
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ describe('Workflow reducer', () => {
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
isInvalidLinkTarget: false,
|
isInvalidLinkTarget: false,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 7000,
|
id: 7000,
|
||||||
name: 'Foo JT',
|
name: 'Foo JT',
|
||||||
},
|
},
|
||||||
@@ -281,7 +281,7 @@ describe('Workflow reducer', () => {
|
|||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
isInvalidLinkTarget: false,
|
isInvalidLinkTarget: false,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 7000,
|
id: 7000,
|
||||||
name: 'Foo JT',
|
name: 'Foo JT',
|
||||||
},
|
},
|
||||||
@@ -869,10 +869,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 2,
|
workflowMakerNodeId: 2,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 1,
|
|
||||||
name: 'JT 1',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -889,10 +885,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 4,
|
workflowMakerNodeId: 4,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 3,
|
|
||||||
name: 'JT 3',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -912,10 +904,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 2,
|
workflowMakerNodeId: 2,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 1,
|
|
||||||
name: 'JT 1',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -932,10 +920,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 3,
|
workflowMakerNodeId: 3,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 2,
|
|
||||||
name: 'JT 2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -955,10 +939,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 5,
|
workflowMakerNodeId: 5,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 4,
|
|
||||||
name: 'JT 4',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -975,17 +955,13 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 3,
|
workflowMakerNodeId: 3,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 2,
|
|
||||||
name: 'JT 2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
linkType: 'always',
|
linkType: 'always',
|
||||||
source: {
|
source: {
|
||||||
id: 1,
|
id: 1,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1004,17 +980,13 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 2,
|
workflowMakerNodeId: 2,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 1,
|
|
||||||
name: 'JT 1',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
linkType: 'always',
|
linkType: 'always',
|
||||||
source: {
|
source: {
|
||||||
id: 1,
|
id: 1,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1033,10 +1005,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 5,
|
workflowMakerNodeId: 5,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 4,
|
|
||||||
name: 'JT 4',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1044,7 +1012,7 @@ describe('Workflow reducer', () => {
|
|||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1063,10 +1031,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 2,
|
workflowMakerNodeId: 2,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 1,
|
|
||||||
name: 'JT 1',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -1083,10 +1047,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 3,
|
workflowMakerNodeId: 3,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 2,
|
|
||||||
name: 'JT 2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -1103,10 +1063,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 4,
|
workflowMakerNodeId: 4,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 3,
|
|
||||||
name: 'JT 3',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@@ -1123,10 +1079,6 @@ describe('Workflow reducer', () => {
|
|||||||
},
|
},
|
||||||
workflowMakerNodeId: 5,
|
workflowMakerNodeId: 5,
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 4,
|
|
||||||
name: 'JT 4',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1718,7 +1670,7 @@ describe('Workflow reducer', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
isInvalidLinkTarget: false,
|
isInvalidLinkTarget: false,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 703,
|
id: 703,
|
||||||
name: 'Test JT',
|
name: 'Test JT',
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
@@ -1729,7 +1681,7 @@ describe('Workflow reducer', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
isInvalidLinkTarget: false,
|
isInvalidLinkTarget: false,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 703,
|
id: 703,
|
||||||
name: 'Test JT',
|
name: 'Test JT',
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
@@ -1757,7 +1709,7 @@ describe('Workflow reducer', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
isEdited: true,
|
isEdited: true,
|
||||||
isInvalidLinkTarget: false,
|
isInvalidLinkTarget: false,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 704,
|
id: 704,
|
||||||
name: 'Other JT',
|
name: 'Other JT',
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { nodePositions } = useContext(WorkflowStateContext);
|
const { nodePositions } = useContext(WorkflowStateContext);
|
||||||
const job = node?.originalNodeObject?.summary_fields?.job;
|
const job = node?.originalNodeObject?.summary_fields?.job;
|
||||||
|
const jobName =
|
||||||
|
node?.originalNodeObject?.summary_fields?.unified_job_template?.name ||
|
||||||
|
node?.unifiedJobTemplate?.name;
|
||||||
|
|
||||||
let borderColor = '#93969A';
|
let borderColor = '#93969A';
|
||||||
|
|
||||||
if (job) {
|
if (job) {
|
||||||
@@ -110,19 +114,15 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) {
|
|||||||
{job ? (
|
{job ? (
|
||||||
<>
|
<>
|
||||||
<JobTopLine>
|
<JobTopLine>
|
||||||
{job.status && <StatusIcon status={job.status} />}
|
{job.status !== 'pending' && <StatusIcon status={job.status} />}
|
||||||
<p>{job.name || node.unifiedJobTemplate.name}</p>
|
<p>{jobName}</p>
|
||||||
</JobTopLine>
|
</JobTopLine>
|
||||||
{!!job?.elapsed && (
|
{!!job?.elapsed && (
|
||||||
<Elapsed>{secondsToHHMMSS(job.elapsed)}</Elapsed>
|
<Elapsed>{secondsToHHMMSS(job.elapsed)}</Elapsed>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<NodeDefaultLabel>
|
<NodeDefaultLabel>{jobName || i18n._(t`DELETED`)}</NodeDefaultLabel>
|
||||||
{node.unifiedJobTemplate
|
|
||||||
? node.unifiedJobTemplate.name
|
|
||||||
: i18n._(t`DELETED`)}
|
|
||||||
</NodeDefaultLabel>
|
|
||||||
)}
|
)}
|
||||||
</NodeContents>
|
</NodeContents>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const nodeWithJT = {
|
|||||||
status: 'successful',
|
status: 'successful',
|
||||||
type: 'job',
|
type: 'job',
|
||||||
},
|
},
|
||||||
|
unified_job_template: {
|
||||||
|
name: 'Automation JT',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
unifiedJobTemplate: {
|
unifiedJobTemplate: {
|
||||||
id: 77,
|
id: 77,
|
||||||
@@ -34,6 +37,9 @@ const nodeWithoutJT = {
|
|||||||
status: 'successful',
|
status: 'successful',
|
||||||
type: 'job',
|
type: 'job',
|
||||||
},
|
},
|
||||||
|
unified_job_template: {
|
||||||
|
name: 'Automation JT 2',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -80,7 +86,7 @@ describe('WorkflowOutputNode', () => {
|
|||||||
</WorkflowStateContext.Provider>
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</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');
|
expect(wrapper.find('WorkflowOutputNode Elapsed').text()).toBe('00:00:07');
|
||||||
});
|
});
|
||||||
test('node contents displayed correctly when Job Template deleted', () => {
|
test('node contents displayed correctly when Job Template deleted', () => {
|
||||||
@@ -95,7 +101,7 @@ describe('WorkflowOutputNode', () => {
|
|||||||
</WorkflowStateContext.Provider>
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</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');
|
expect(wrapper.find('WorkflowOutputNode Elapsed').text()).toBe('00:00:07');
|
||||||
});
|
});
|
||||||
test('node contents displayed correctly when Job deleted', () => {
|
test('node contents displayed correctly when Job deleted', () => {
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ function SurveyPreviewModal({
|
|||||||
isReadOnly
|
isReadOnly
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
isOpen={false}
|
isOpen={false}
|
||||||
selections={q.default.length > 0 && q.default.split('\n')}
|
selections={
|
||||||
|
q.default.length > 0 ? q.default.split('\n') : []
|
||||||
|
}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
aria-label={i18n._(t`Multi-Select`)}
|
aria-label={i18n._(t`Multi-Select`)}
|
||||||
id={`survey-preview-multiSelect-${q.variable}`}
|
id={`survey-preview-multiSelect-${q.variable}`}
|
||||||
|
|||||||
@@ -6,18 +6,56 @@ import {
|
|||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
} from '../../../../../contexts/Workflow';
|
} from '../../../../../contexts/Workflow';
|
||||||
import NodeModal from './NodeModal';
|
import NodeModal from './NodeModal';
|
||||||
|
import { getAddedAndRemoved } from '../../../../../util/lists';
|
||||||
|
|
||||||
function NodeAddModal({ i18n }) {
|
function NodeAddModal({ i18n }) {
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const { addNodeSource } = useContext(WorkflowStateContext);
|
const { addNodeSource } = useContext(WorkflowStateContext);
|
||||||
|
|
||||||
const addNode = (resource, linkType) => {
|
const addNode = (values, config) => {
|
||||||
|
const {
|
||||||
|
approvalName,
|
||||||
|
approvalDescription,
|
||||||
|
timeoutMinutes,
|
||||||
|
timeoutSeconds,
|
||||||
|
linkType,
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
if (values) {
|
||||||
|
const { added, removed } = getAddedAndRemoved(
|
||||||
|
config?.defaults?.credentials,
|
||||||
|
values?.credentials
|
||||||
|
);
|
||||||
|
|
||||||
|
values.addedCredentials = added;
|
||||||
|
values.removedCredentials = removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.nodeResource = values.nodeResource;
|
||||||
|
if (
|
||||||
|
values?.nodeType === 'job_template' ||
|
||||||
|
values?.nodeType === 'workflow_job_template'
|
||||||
|
) {
|
||||||
|
node.promptValues = values;
|
||||||
|
}
|
||||||
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'CREATE_NODE',
|
type: 'CREATE_NODE',
|
||||||
node: {
|
node,
|
||||||
linkType,
|
|
||||||
nodeResource: resource,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../../testUtils/enzymeHelpers';
|
||||||
import {
|
import {
|
||||||
WorkflowDispatchContext,
|
WorkflowDispatchContext,
|
||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
@@ -13,6 +16,9 @@ const nodeResource = {
|
|||||||
id: 448,
|
id: 448,
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
name: 'Test JT',
|
name: 'Test JT',
|
||||||
|
summary_fields: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflowContext = {
|
const workflowContext = {
|
||||||
@@ -20,23 +26,37 @@ const workflowContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('NodeAddModal', () => {
|
describe('NodeAddModal', () => {
|
||||||
test('Node modal confirmation dispatches as expected', () => {
|
test('Node modal confirmation dispatches as expected', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
<WorkflowStateContext.Provider value={workflowContext}>
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
<NodeAddModal />
|
<NodeAddModal onSave={() => {}} askLinkType title="Add Node" />
|
||||||
</WorkflowStateContext.Provider>
|
</WorkflowStateContext.Provider>
|
||||||
</WorkflowDispatchContext.Provider>
|
</WorkflowDispatchContext.Provider>
|
||||||
);
|
);
|
||||||
act(() => {
|
waitForElement(
|
||||||
wrapper.find('NodeModal').prop('onSave')(nodeResource, 'success');
|
wrapper,
|
||||||
|
'WizardNavItem[content="ContentLoading"]',
|
||||||
|
el => el.length === 0
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('NodeModal').prop('onSave')(
|
||||||
|
{ linkType: 'success', nodeResource },
|
||||||
|
{}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: 'CREATE_NODE',
|
|
||||||
node: {
|
node: {
|
||||||
linkType: 'success',
|
linkType: 'success',
|
||||||
nodeResource,
|
nodeResource: {
|
||||||
|
id: 448,
|
||||||
|
name: 'Test JT',
|
||||||
|
summary_fields: { credentials: [] },
|
||||||
|
type: 'job_template',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
type: 'CREATE_NODE',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,12 +7,45 @@ import NodeModal from './NodeModal';
|
|||||||
function NodeEditModal({ i18n }) {
|
function NodeEditModal({ i18n }) {
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
|
||||||
const updateNode = resource => {
|
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({
|
dispatch({
|
||||||
type: 'UPDATE_NODE',
|
type: 'UPDATE_NODE',
|
||||||
node: {
|
node,
|
||||||
nodeResource: resource,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../../../testUtils/enzymeHelpers';
|
||||||
import {
|
import {
|
||||||
WorkflowDispatchContext,
|
WorkflowDispatchContext,
|
||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
@@ -9,10 +12,17 @@ import NodeEditModal from './NodeEditModal';
|
|||||||
|
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
|
|
||||||
const nodeResource = {
|
jest.mock('../../../../../api/models/InventorySources');
|
||||||
|
jest.mock('../../../../../api/models/JobTemplates');
|
||||||
|
jest.mock('../../../../../api/models/Projects');
|
||||||
|
jest.mock('../../../../../api/models/WorkflowJobTemplates');
|
||||||
|
const values = {
|
||||||
|
inventory: undefined,
|
||||||
|
nodeResource: {
|
||||||
id: 448,
|
id: 448,
|
||||||
type: 'job_template',
|
|
||||||
name: 'Test JT',
|
name: 'Test JT',
|
||||||
|
type: 'job_template',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflowContext = {
|
const workflowContext = {
|
||||||
@@ -22,27 +32,40 @@ const workflowContext = {
|
|||||||
id: 30,
|
id: 30,
|
||||||
name: 'Foo JT',
|
name: 'Foo JT',
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
|
unified_job_type: 'job',
|
||||||
|
},
|
||||||
|
originalNodeObject: {
|
||||||
|
summary_fields: { unified_job_template: { id: 1, name: 'Job Template' } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('NodeEditModal', () => {
|
describe('NodeEditModal', () => {
|
||||||
test('Node modal confirmation dispatches as expected', () => {
|
test('Node modal confirmation dispatches as expected', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
<WorkflowStateContext.Provider value={workflowContext}>
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
<NodeEditModal />
|
<NodeEditModal
|
||||||
|
onSave={() => {}}
|
||||||
|
askLinkType={false}
|
||||||
|
title="Edit Node"
|
||||||
|
/>
|
||||||
</WorkflowStateContext.Provider>
|
</WorkflowStateContext.Provider>
|
||||||
</WorkflowDispatchContext.Provider>
|
</WorkflowDispatchContext.Provider>
|
||||||
);
|
);
|
||||||
act(() => {
|
waitForElement(
|
||||||
wrapper.find('NodeModal').prop('onSave')(nodeResource);
|
wrapper,
|
||||||
|
'WizardNavItem[content="ContentLoading"]',
|
||||||
|
el => el.length === 0
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('NodeModal').prop('onSave')(values, {});
|
||||||
});
|
});
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: 'UPDATE_NODE',
|
|
||||||
node: {
|
node: {
|
||||||
nodeResource,
|
nodeResource: { id: 448, name: 'Test JT', type: 'job_template' },
|
||||||
},
|
},
|
||||||
|
type: 'UPDATE_NODE',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,86 +1,51 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState, useEffect, useCallback } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { Formik, useFormikContext } from 'formik';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
import { bool, node, func } from 'prop-types';
|
import { bool, node, func } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
WizardContextConsumer,
|
WizardContextConsumer,
|
||||||
WizardFooter,
|
WizardFooter,
|
||||||
|
Form,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import ContentError from '../../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../../components/ContentLoading';
|
||||||
|
|
||||||
|
import useRequest, {
|
||||||
|
useDismissableError,
|
||||||
|
} from '../../../../../util/useRequest';
|
||||||
|
import mergeExtraVars from '../../../../../util/prompt/mergeExtraVars';
|
||||||
|
import getSurveyValues from '../../../../../util/prompt/getSurveyValues';
|
||||||
|
import { parseVariableField } from '../../../../../util/yaml';
|
||||||
import {
|
import {
|
||||||
WorkflowDispatchContext,
|
WorkflowDispatchContext,
|
||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
} from '../../../../../contexts/Workflow';
|
} from '../../../../../contexts/Workflow';
|
||||||
|
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../../../api';
|
||||||
import Wizard from '../../../../../components/Wizard';
|
import Wizard from '../../../../../components/Wizard';
|
||||||
import { NodeTypeStep } from './NodeTypeStep';
|
import useWorkflowNodeSteps from './useWorkflowNodeSteps';
|
||||||
import RunStep from './RunStep';
|
import AlertModal from '../../../../../components/AlertModal';
|
||||||
|
|
||||||
import NodeNextButton from './NodeNextButton';
|
import NodeNextButton from './NodeNextButton';
|
||||||
|
|
||||||
function NodeModal({ askLinkType, i18n, onSave, title }) {
|
function NodeModalForm({
|
||||||
|
askLinkType,
|
||||||
|
i18n,
|
||||||
|
onSave,
|
||||||
|
title,
|
||||||
|
credentialError,
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
isLaunchLoading,
|
||||||
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const { nodeToEdit } = useContext(WorkflowStateContext);
|
const { values, setTouched, validateForm } = useFormikContext();
|
||||||
|
|
||||||
let defaultApprovalDescription = '';
|
|
||||||
let defaultApprovalName = '';
|
|
||||||
let defaultApprovalTimeout = 0;
|
|
||||||
let defaultNodeResource = null;
|
|
||||||
let defaultNodeType = 'job_template';
|
|
||||||
if (nodeToEdit && nodeToEdit.unifiedJobTemplate) {
|
|
||||||
if (
|
|
||||||
nodeToEdit &&
|
|
||||||
nodeToEdit.unifiedJobTemplate &&
|
|
||||||
(nodeToEdit.unifiedJobTemplate.type ||
|
|
||||||
nodeToEdit.unifiedJobTemplate.unified_job_type)
|
|
||||||
) {
|
|
||||||
const ujtType =
|
|
||||||
nodeToEdit.unifiedJobTemplate.type ||
|
|
||||||
nodeToEdit.unifiedJobTemplate.unified_job_type;
|
|
||||||
switch (ujtType) {
|
|
||||||
case 'job_template':
|
|
||||||
case 'job':
|
|
||||||
defaultNodeType = 'job_template';
|
|
||||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
|
||||||
break;
|
|
||||||
case 'project':
|
|
||||||
case 'project_update':
|
|
||||||
defaultNodeType = 'project_sync';
|
|
||||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
|
||||||
break;
|
|
||||||
case 'inventory_source':
|
|
||||||
case 'inventory_update':
|
|
||||||
defaultNodeType = 'inventory_source_sync';
|
|
||||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
|
||||||
break;
|
|
||||||
case 'workflow_job_template':
|
|
||||||
case 'workflow_job':
|
|
||||||
defaultNodeType = 'workflow_job_template';
|
|
||||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
|
||||||
break;
|
|
||||||
case 'workflow_approval_template':
|
|
||||||
case 'workflow_approval':
|
|
||||||
defaultNodeType = 'approval';
|
|
||||||
defaultApprovalName = nodeToEdit.unifiedJobTemplate.name;
|
|
||||||
defaultApprovalDescription =
|
|
||||||
nodeToEdit.unifiedJobTemplate.description;
|
|
||||||
defaultApprovalTimeout = nodeToEdit.unifiedJobTemplate.timeout;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const [approvalDescription, setApprovalDescription] = useState(
|
|
||||||
defaultApprovalDescription
|
|
||||||
);
|
|
||||||
const [approvalName, setApprovalName] = useState(defaultApprovalName);
|
|
||||||
const [approvalTimeout, setApprovalTimeout] = useState(
|
|
||||||
defaultApprovalTimeout
|
|
||||||
);
|
|
||||||
const [linkType, setLinkType] = useState('success');
|
|
||||||
const [nodeResource, setNodeResource] = useState(defaultNodeResource);
|
|
||||||
const [nodeType, setNodeType] = useState(defaultNodeType);
|
|
||||||
const [triggerNext, setTriggerNext] = useState(0);
|
const [triggerNext, setTriggerNext] = useState(0);
|
||||||
|
|
||||||
const clearQueryParams = () => {
|
const clearQueryParams = () => {
|
||||||
@@ -93,20 +58,48 @@ function NodeModal({ askLinkType, i18n, onSave, title }) {
|
|||||||
history.replace(`${history.location.pathname}?${otherParts.join('&')}`);
|
history.replace(`${history.location.pathname}?${otherParts.join('&')}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
steps: promptSteps,
|
||||||
|
visitStep,
|
||||||
|
visitAllSteps,
|
||||||
|
contentError,
|
||||||
|
} = useWorkflowNodeSteps(
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
i18n,
|
||||||
|
values.nodeResource,
|
||||||
|
askLinkType
|
||||||
|
);
|
||||||
|
|
||||||
const handleSaveNode = () => {
|
const handleSaveNode = () => {
|
||||||
clearQueryParams();
|
clearQueryParams();
|
||||||
|
if (values.nodeType !== 'workflow_approval_template') {
|
||||||
const resource =
|
delete values.approvalName;
|
||||||
nodeType === 'approval'
|
delete values.approvalDescription;
|
||||||
? {
|
delete values.timeoutMinutes;
|
||||||
description: approvalDescription,
|
delete values.timeoutSeconds;
|
||||||
name: approvalName,
|
|
||||||
timeout: approvalTimeout,
|
|
||||||
type: 'workflow_approval_template',
|
|
||||||
}
|
}
|
||||||
: nodeResource;
|
|
||||||
|
|
||||||
onSave(resource, askLinkType ? linkType : null);
|
if (
|
||||||
|
['job_template', 'workflow_job_template'].includes(values.nodeType) &&
|
||||||
|
(launchConfig.ask_variables_on_launch || launchConfig.survey_enabled)
|
||||||
|
) {
|
||||||
|
let extraVars;
|
||||||
|
const surveyValues = getSurveyValues(values);
|
||||||
|
const initialExtraVars =
|
||||||
|
launchConfig.ask_variables_on_launch && (values.extra_vars || '---');
|
||||||
|
if (surveyConfig?.spec) {
|
||||||
|
extraVars = yaml.safeDump(
|
||||||
|
mergeExtraVars(initialExtraVars, surveyValues)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {}));
|
||||||
|
}
|
||||||
|
values.extra_data = extraVars && parseVariableField(extraVars);
|
||||||
|
delete values.extra_vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(values, launchConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -114,53 +107,15 @@ function NodeModal({ askLinkType, i18n, onSave, title }) {
|
|||||||
dispatch({ type: 'CANCEL_NODE_MODAL' });
|
dispatch({ type: 'CANCEL_NODE_MODAL' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNodeTypeChange = newNodeType => {
|
const { error, dismissError } = useDismissableError(
|
||||||
setNodeType(newNodeType);
|
contentError || credentialError
|
||||||
setNodeResource(null);
|
);
|
||||||
setApprovalName('');
|
|
||||||
setApprovalDescription('');
|
|
||||||
setApprovalTimeout(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const steps = [
|
const nextButtonText = activeStep =>
|
||||||
...(askLinkType
|
activeStep.id === promptSteps[promptSteps?.length - 1]?.id ||
|
||||||
? [
|
activeStep.name === 'Preview'
|
||||||
{
|
? i18n._(t`Save`)
|
||||||
name: i18n._(t`Run Type`),
|
: i18n._(t`Next`);
|
||||||
key: 'run_type',
|
|
||||||
component: (
|
|
||||||
<RunStep linkType={linkType} onUpdateLinkType={setLinkType} />
|
|
||||||
),
|
|
||||||
enableNext: linkType !== null,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
name: i18n._(t`Node Type`),
|
|
||||||
key: 'node_resource',
|
|
||||||
enableNext:
|
|
||||||
(nodeType !== 'approval' && nodeResource !== null) ||
|
|
||||||
(nodeType === 'approval' && approvalName !== ''),
|
|
||||||
component: (
|
|
||||||
<NodeTypeStep
|
|
||||||
description={approvalDescription}
|
|
||||||
name={approvalName}
|
|
||||||
nodeResource={nodeResource}
|
|
||||||
nodeType={nodeType}
|
|
||||||
onUpdateDescription={setApprovalDescription}
|
|
||||||
onUpdateName={setApprovalName}
|
|
||||||
onUpdateNodeResource={setNodeResource}
|
|
||||||
onUpdateNodeType={handleNodeTypeChange}
|
|
||||||
onUpdateTimeout={setApprovalTimeout}
|
|
||||||
timeout={approvalTimeout}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
steps.forEach((step, n) => {
|
|
||||||
step.id = n + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
const CustomFooter = (
|
const CustomFooter = (
|
||||||
<WizardFooter>
|
<WizardFooter>
|
||||||
@@ -168,24 +123,28 @@ function NodeModal({ askLinkType, i18n, onSave, title }) {
|
|||||||
{({ activeStep, onNext, onBack }) => (
|
{({ activeStep, onNext, onBack }) => (
|
||||||
<>
|
<>
|
||||||
<NodeNextButton
|
<NodeNextButton
|
||||||
|
isDisabled={isLaunchLoading}
|
||||||
triggerNext={triggerNext}
|
triggerNext={triggerNext}
|
||||||
activeStep={activeStep}
|
activeStep={activeStep}
|
||||||
|
aria-label={nextButtonText(activeStep)}
|
||||||
onNext={onNext}
|
onNext={onNext}
|
||||||
onClick={() => setTriggerNext(triggerNext + 1)}
|
onClick={() => setTriggerNext(triggerNext + 1)}
|
||||||
buttonText={
|
buttonText={nextButtonText(activeStep)}
|
||||||
activeStep.key === 'node_resource'
|
|
||||||
? i18n._(t`Save`)
|
|
||||||
: i18n._(t`Next`)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{activeStep && activeStep.id !== 1 && (
|
{activeStep && activeStep.id !== promptSteps[0]?.id && (
|
||||||
<Button id="back-node-modal" variant="secondary" onClick={onBack}>
|
<Button
|
||||||
|
id="back-node-modal"
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={i18n._(t`Back`)}
|
||||||
|
onClick={onBack}
|
||||||
|
>
|
||||||
{i18n._(t`Back`)}
|
{i18n._(t`Back`)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
id="cancel-node-modal"
|
id="cancel-node-modal"
|
||||||
variant="link"
|
variant="link"
|
||||||
|
aria-label={i18n._(t`Cancel`)}
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
@@ -196,21 +155,206 @@ function NodeModal({ askLinkType, i18n, onSave, title }) {
|
|||||||
</WizardFooter>
|
</WizardFooter>
|
||||||
);
|
);
|
||||||
|
|
||||||
const wizardTitle = nodeResource ? `${title} | ${nodeResource.name}` : title;
|
if (error) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => {
|
||||||
|
dismissError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentError error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && !isLaunchLoading) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => {
|
||||||
|
dismissError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentError error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Wizard
|
<Wizard
|
||||||
footer={CustomFooter}
|
footer={CustomFooter}
|
||||||
isOpen
|
isOpen={!error}
|
||||||
onClose={handleCancel}
|
onClose={handleCancel}
|
||||||
onSave={handleSaveNode}
|
onSave={() => {
|
||||||
steps={steps}
|
handleSaveNode();
|
||||||
|
}}
|
||||||
|
onGoToStep={async (nextStep, prevStep) => {
|
||||||
|
if (nextStep.id === 'preview') {
|
||||||
|
visitAllSteps(setTouched);
|
||||||
|
} else {
|
||||||
|
visitStep(prevStep.prevId);
|
||||||
|
}
|
||||||
|
await validateForm();
|
||||||
|
}}
|
||||||
|
steps={promptSteps}
|
||||||
css="overflow: scroll"
|
css="overflow: scroll"
|
||||||
title={wizardTitle}
|
title={title}
|
||||||
|
onNext={async (nextStep, prevStep) => {
|
||||||
|
if (nextStep.id === 'preview') {
|
||||||
|
visitAllSteps(setTouched);
|
||||||
|
} else {
|
||||||
|
visitStep(prevStep.prevId);
|
||||||
|
}
|
||||||
|
await validateForm();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, config) => {
|
||||||
|
onSave(values, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
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}>
|
||||||
|
<NodeModalInner
|
||||||
|
onSave={onSaveForm}
|
||||||
|
i18n={i18n}
|
||||||
|
title={title}
|
||||||
|
askLinkType={askLinkType}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
NodeModal.propTypes = {
|
NodeModal.propTypes = {
|
||||||
askLinkType: bool.isRequired,
|
askLinkType: bool.isRequired,
|
||||||
onSave: func.isRequired,
|
onSave: func.isRequired,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
} from '../../../../../api';
|
} from '../../../../../api';
|
||||||
import NodeModal from './NodeModal';
|
import NodeModal from './NodeModal';
|
||||||
|
|
||||||
|
jest.mock('../../../../../api/models/Credentials');
|
||||||
|
jest.mock('../../../../../api/models/Inventories');
|
||||||
jest.mock('../../../../../api/models/InventorySources');
|
jest.mock('../../../../../api/models/InventorySources');
|
||||||
jest.mock('../../../../../api/models/JobTemplates');
|
jest.mock('../../../../../api/models/JobTemplates');
|
||||||
jest.mock('../../../../../api/models/Projects');
|
jest.mock('../../../../../api/models/Projects');
|
||||||
@@ -25,19 +27,81 @@ let wrapper;
|
|||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const onSave = 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', () => {
|
describe('NodeModal', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
JobTemplatesAPI.read.mockResolvedValue({
|
JobTemplatesAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
count: 1,
|
count: 1,
|
||||||
results: [
|
results: [mockJobTemplate],
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Test Job Template',
|
|
||||||
type: 'job_template',
|
|
||||||
url: '/api/v2/job_templates/1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
JobTemplatesAPI.readOptions.mockResolvedValue({
|
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||||
@@ -49,6 +113,24 @@ describe('NodeModal', () => {
|
|||||||
related_search_fields: [],
|
related_search_fields: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
JobTemplatesAPI.readLaunch.mockResolvedValue({ data: jtLaunchConfig });
|
||||||
|
JobTemplatesAPI.readSurvey.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
spec: [
|
||||||
|
{
|
||||||
|
question_name: 'Foo',
|
||||||
|
required: true,
|
||||||
|
variable: 'bar',
|
||||||
|
type: 'text',
|
||||||
|
default: 'answer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'text',
|
||||||
|
variable: 'bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
ProjectsAPI.read.mockResolvedValue({
|
ProjectsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
count: 1,
|
count: 1,
|
||||||
@@ -116,6 +198,33 @@ describe('NodeModal', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
ask_inventory_on_launch: false,
|
||||||
|
ask_limit_on_launch: false,
|
||||||
|
ask_scm_branch_on_launch: false,
|
||||||
|
can_start_without_user_input: false,
|
||||||
|
defaults: {
|
||||||
|
extra_vars: '---',
|
||||||
|
inventory: {
|
||||||
|
name: null,
|
||||||
|
id: null,
|
||||||
|
},
|
||||||
|
limit: '',
|
||||||
|
scm_branch: '',
|
||||||
|
},
|
||||||
|
survey_enabled: false,
|
||||||
|
variables_needed_to_start: [],
|
||||||
|
node_templates_missing: [],
|
||||||
|
node_prompts_rejected: [272, 273],
|
||||||
|
workflow_job_template_data: {
|
||||||
|
name: 'jt',
|
||||||
|
id: 53,
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
ask_variables_on_launch: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@@ -137,8 +246,9 @@ describe('NodeModal', () => {
|
|||||||
await waitForElement(wrapper, 'PFWizard');
|
await waitForElement(wrapper, 'PFWizard');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
onSave.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can successfully create a new job template node', async () => {
|
test('Can successfully create a new job template node', async () => {
|
||||||
@@ -149,18 +259,58 @@ describe('NodeModal', () => {
|
|||||||
wrapper.find('button#next-node-modal').simulate('click');
|
wrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
wrapper.find('Radio').simulate('click');
|
wrapper.find('Radio').simulate('click');
|
||||||
|
});
|
||||||
|
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('Next');
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('StepName#preview-step').simulate('click');
|
||||||
|
});
|
||||||
|
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 () => {
|
await act(async () => {
|
||||||
wrapper.find('button#next-node-modal').simulate('click');
|
wrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
id: 1,
|
linkType: 'always',
|
||||||
name: 'Test Job Template',
|
nodeType: 'job_template',
|
||||||
type: 'job_template',
|
inventory: { name: 'Foo Inv', id: 1 },
|
||||||
url: '/api/v2/job_templates/1',
|
credentials: [],
|
||||||
|
job_type: '',
|
||||||
|
verbosity: '0',
|
||||||
|
job_tags: '',
|
||||||
|
skip_tags: '',
|
||||||
|
diff_mode: false,
|
||||||
|
survey_bar: 'answer',
|
||||||
|
nodeResource: mockJobTemplate,
|
||||||
|
extra_data: { bar: 'answer' },
|
||||||
},
|
},
|
||||||
'always'
|
jtLaunchConfig
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,21 +323,29 @@ describe('NodeModal', () => {
|
|||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect').prop('onChange')(null, 'project_sync');
|
wrapper.find('AnsibleSelect').prop('onChange')(null, 'project');
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
wrapper.find('Radio').simulate('click');
|
wrapper.find('Radio').simulate('click');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button#next-node-modal').simulate('click');
|
wrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
|
linkType: 'failure',
|
||||||
|
nodeResource: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Project',
|
name: 'Test Project',
|
||||||
type: 'project',
|
type: 'project',
|
||||||
url: '/api/v2/projects/1',
|
url: '/api/v2/projects/1',
|
||||||
},
|
},
|
||||||
'failure'
|
nodeType: 'project',
|
||||||
|
verbosity: undefined,
|
||||||
|
},
|
||||||
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,22 +360,30 @@ describe('NodeModal', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect').prop('onChange')(
|
wrapper.find('AnsibleSelect').prop('onChange')(
|
||||||
null,
|
null,
|
||||||
'inventory_source_sync'
|
'inventory_source'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
wrapper.find('Radio').simulate('click');
|
wrapper.find('Radio').simulate('click');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button#next-node-modal').simulate('click');
|
wrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
|
linkType: 'failure',
|
||||||
|
nodeResource: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Inventory Source',
|
name: 'Test Inventory Source',
|
||||||
type: 'inventory_source',
|
type: 'inventory_source',
|
||||||
url: '/api/v2/inventory_sources/1',
|
url: '/api/v2/inventory_sources/1',
|
||||||
},
|
},
|
||||||
'failure'
|
nodeType: 'inventory_source',
|
||||||
|
verbosity: undefined,
|
||||||
|
},
|
||||||
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,18 +399,47 @@ describe('NodeModal', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
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');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button#next-node-modal').simulate('click');
|
wrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
|
linkType: 'success',
|
||||||
|
nodeResource: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Workflow Job Template',
|
name: 'Test Workflow Job Template',
|
||||||
type: 'workflow_job_template',
|
type: 'workflow_job_template',
|
||||||
url: '/api/v2/workflow_job_templates/1',
|
url: '/api/v2/workflow_job_templates/1',
|
||||||
},
|
},
|
||||||
'success'
|
nodeType: 'workflow_job_template',
|
||||||
|
verbosity: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ask_inventory_on_launch: false,
|
||||||
|
ask_limit_on_launch: false,
|
||||||
|
ask_scm_branch_on_launch: false,
|
||||||
|
ask_variables_on_launch: false,
|
||||||
|
can_start_without_user_input: false,
|
||||||
|
defaults: {
|
||||||
|
extra_vars: '---',
|
||||||
|
inventory: { id: null, name: null },
|
||||||
|
limit: '',
|
||||||
|
scm_branch: '',
|
||||||
|
},
|
||||||
|
node_prompts_rejected: [272, 273],
|
||||||
|
node_templates_missing: [],
|
||||||
|
survey_enabled: false,
|
||||||
|
variables_needed_to_start: [],
|
||||||
|
workflow_job_template_data: { description: '', id: 53, name: 'jt' },
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,26 +452,26 @@ describe('NodeModal', () => {
|
|||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect').prop('onChange')(null, 'approval');
|
wrapper.find('AnsibleSelect').prop('onChange')(
|
||||||
|
null,
|
||||||
|
'workflow_approval_template'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('input#approval-name').simulate('change', {
|
wrapper.find('input#approval-name').simulate('change', {
|
||||||
target: { value: 'Test Approval', name: 'name' },
|
target: { value: 'Test Approval', name: 'approvalName' },
|
||||||
});
|
});
|
||||||
wrapper.find('input#approval-description').simulate('change', {
|
wrapper.find('input#approval-description').simulate('change', {
|
||||||
target: { value: 'Test Approval Description', name: 'description' },
|
target: {
|
||||||
|
value: 'Test Approval Description',
|
||||||
|
name: 'approvalDescription',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
wrapper.find('input#approval-timeout-minutes').simulate('change', {
|
wrapper.find('input#approval-timeout-minutes').simulate('change', {
|
||||||
target: { value: 5, name: 'timeoutMinutes' },
|
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', {
|
wrapper.find('input#approval-timeout-seconds').simulate('change', {
|
||||||
target: { value: 30, name: 'timeoutSeconds' },
|
target: { value: 30, name: 'timeoutSeconds' },
|
||||||
});
|
});
|
||||||
@@ -301,12 +496,15 @@ describe('NodeModal', () => {
|
|||||||
});
|
});
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
description: 'Test Approval Description',
|
approvalDescription: 'Test Approval Description',
|
||||||
name: 'Test Approval',
|
approvalName: 'Test Approval',
|
||||||
timeout: 330,
|
linkType: 'always',
|
||||||
type: 'workflow_approval_template',
|
nodeResource: null,
|
||||||
|
nodeType: 'workflow_approval_template',
|
||||||
|
timeoutMinutes: 5,
|
||||||
|
timeoutSeconds: 30,
|
||||||
},
|
},
|
||||||
'always'
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,22 +516,24 @@ describe('NodeModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Edit existing node', () => {
|
describe('Edit existing node', () => {
|
||||||
|
let newWrapper;
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
newWrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can successfully change project sync node to workflow approval node', async () => {
|
test('Can successfully change project sync node to workflow approval node', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
newWrapper = mountWithContexts(
|
||||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
<WorkflowStateContext.Provider
|
<WorkflowStateContext.Provider
|
||||||
value={{
|
value={{
|
||||||
nodeToEdit: {
|
nodeToEdit: {
|
||||||
id: 2,
|
id: 2,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Project',
|
name: 'Test Project',
|
||||||
unified_job_type: 'project_update',
|
type: 'project',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -347,74 +547,78 @@ describe('NodeModal', () => {
|
|||||||
</WorkflowDispatchContext.Provider>
|
</WorkflowDispatchContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'PFWizard');
|
await waitForElement(newWrapper, 'PFWizard');
|
||||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('project_sync');
|
newWrapper.update();
|
||||||
|
expect(newWrapper.find('AnsibleSelect').prop('value')).toBe('project');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect').prop('onChange')(null, 'approval');
|
newWrapper.find('AnsibleSelect').prop('onChange')(
|
||||||
|
null,
|
||||||
|
'workflow_approval_template'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
newWrapper.update();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('input#approval-name').simulate('change', {
|
newWrapper.find('input#approval-name').simulate('change', {
|
||||||
target: { value: 'Test Approval', name: 'name' },
|
target: { value: 'Test Approval', name: 'approvalName' },
|
||||||
});
|
});
|
||||||
wrapper.find('input#approval-description').simulate('change', {
|
newWrapper.find('input#approval-description').simulate('change', {
|
||||||
target: { value: 'Test Approval Description', name: 'description' },
|
target: {
|
||||||
|
value: 'Test Approval Description',
|
||||||
|
name: 'approvalDescription',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
wrapper.find('input#approval-timeout-minutes').simulate('change', {
|
newWrapper.find('input#approval-timeout-minutes').simulate('change', {
|
||||||
target: { value: 5, name: 'timeoutMinutes' },
|
target: { value: 5, name: 'timeoutMinutes' },
|
||||||
});
|
});
|
||||||
});
|
newWrapper.find('input#approval-timeout-seconds').simulate('change', {
|
||||||
|
|
||||||
// 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' },
|
target: { value: 30, name: 'timeoutSeconds' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
wrapper.update();
|
newWrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('input#approval-name').prop('value')).toBe(
|
expect(newWrapper.find('input#approval-name').prop('value')).toBe(
|
||||||
'Test Approval'
|
'Test Approval'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('input#approval-description').prop('value')).toBe(
|
expect(newWrapper.find('input#approval-description').prop('value')).toBe(
|
||||||
'Test Approval Description'
|
'Test Approval Description'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('input#approval-timeout-minutes').prop('value')).toBe(
|
expect(
|
||||||
5
|
newWrapper.find('input#approval-timeout-minutes').prop('value')
|
||||||
);
|
).toBe(5);
|
||||||
expect(wrapper.find('input#approval-timeout-seconds').prop('value')).toBe(
|
expect(
|
||||||
30
|
newWrapper.find('input#approval-timeout-seconds').prop('value')
|
||||||
);
|
).toBe(30);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button#next-node-modal').simulate('click');
|
newWrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
description: 'Test Approval Description',
|
approvalDescription: 'Test Approval Description',
|
||||||
name: 'Test Approval',
|
approvalName: 'Test Approval',
|
||||||
timeout: 330,
|
linkType: 'success',
|
||||||
type: 'workflow_approval_template',
|
nodeResource: null,
|
||||||
|
nodeType: 'workflow_approval_template',
|
||||||
|
timeoutMinutes: 5,
|
||||||
|
timeoutSeconds: 30,
|
||||||
},
|
},
|
||||||
null
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can successfully change approval node to workflow job template node', async () => {
|
test('Can successfully change approval node to workflow job template node', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
newWrapper = mountWithContexts(
|
||||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
<WorkflowStateContext.Provider
|
<WorkflowStateContext.Provider
|
||||||
value={{
|
value={{
|
||||||
nodeToEdit: {
|
nodeToEdit: {
|
||||||
id: 2,
|
id: 2,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Approval',
|
name: 'Test Approval',
|
||||||
description: 'Test Approval Description',
|
description: 'Test Approval Description',
|
||||||
unified_job_type: 'workflow_approval',
|
type: 'workflow_approval_template',
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -429,27 +633,56 @@ describe('NodeModal', () => {
|
|||||||
</WorkflowDispatchContext.Provider>
|
</WorkflowDispatchContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'PFWizard');
|
await waitForElement(newWrapper, 'PFWizard');
|
||||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('approval');
|
expect(newWrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||||
|
'workflow_approval_template'
|
||||||
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect').prop('onChange')(
|
newWrapper.find('AnsibleSelect').prop('onChange')(
|
||||||
null,
|
null,
|
||||||
'workflow_job_template'
|
'workflow_job_template'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
newWrapper.update();
|
||||||
wrapper.find('Radio').simulate('click');
|
await act(async () => newWrapper.find('Radio').simulate('click'));
|
||||||
|
newWrapper.update();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button#next-node-modal').simulate('click');
|
newWrapper.find('button#next-node-modal').simulate('click');
|
||||||
|
});
|
||||||
|
newWrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
newWrapper.find('button#next-node-modal').simulate('click');
|
||||||
});
|
});
|
||||||
expect(onSave).toBeCalledWith(
|
expect(onSave).toBeCalledWith(
|
||||||
{
|
{
|
||||||
|
linkType: 'success',
|
||||||
|
nodeResource: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Workflow Job Template',
|
name: 'Test Workflow Job Template',
|
||||||
type: 'workflow_job_template',
|
type: 'workflow_job_template',
|
||||||
url: '/api/v2/workflow_job_templates/1',
|
url: '/api/v2/workflow_job_templates/1',
|
||||||
},
|
},
|
||||||
null
|
nodeType: 'workflow_job_template',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ask_inventory_on_launch: false,
|
||||||
|
ask_limit_on_launch: false,
|
||||||
|
ask_scm_branch_on_launch: false,
|
||||||
|
ask_variables_on_launch: false,
|
||||||
|
can_start_without_user_input: false,
|
||||||
|
defaults: {
|
||||||
|
extra_vars: '---',
|
||||||
|
inventory: { id: null, name: null },
|
||||||
|
limit: '',
|
||||||
|
scm_branch: '',
|
||||||
|
},
|
||||||
|
node_prompts_rejected: [272, 273],
|
||||||
|
node_templates_missing: [],
|
||||||
|
survey_enabled: false,
|
||||||
|
variables_needed_to_start: [],
|
||||||
|
workflow_job_template_data: { description: '', id: 53, name: 'jt' },
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { func, number, shape, string } from 'prop-types';
|
import { func, oneOfType, number, shape, string } from 'prop-types';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
|
|
||||||
function NodeNextButton({
|
function NodeNextButton({
|
||||||
@@ -8,6 +8,7 @@ function NodeNextButton({
|
|||||||
onClick,
|
onClick,
|
||||||
onNext,
|
onNext,
|
||||||
triggerNext,
|
triggerNext,
|
||||||
|
isDisabled,
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!triggerNext) {
|
if (!triggerNext) {
|
||||||
@@ -22,7 +23,7 @@ function NodeNextButton({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onClick(activeStep)}
|
onClick={() => onClick(activeStep)}
|
||||||
isDisabled={!activeStep.enableNext}
|
isDisabled={isDisabled || !activeStep.enableNext}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -34,7 +35,7 @@ NodeNextButton.propTypes = {
|
|||||||
buttonText: string.isRequired,
|
buttonText: string.isRequired,
|
||||||
onClick: func.isRequired,
|
onClick: func.isRequired,
|
||||||
onNext: func.isRequired,
|
onNext: func.isRequired,
|
||||||
triggerNext: number.isRequired,
|
triggerNext: oneOfType([string, number]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NodeNextButton;
|
export default NodeNextButton;
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import 'styled-components/macro';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t, Trans } from '@lingui/macro';
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { func, number, shape, string } from 'prop-types';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Formik, Field } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||||
|
import { required } from '../../../../../../util/validators';
|
||||||
|
|
||||||
import { FormFullWidthLayout } from '../../../../../../components/FormLayout';
|
import { FormFullWidthLayout } from '../../../../../../components/FormLayout';
|
||||||
import AnsibleSelect from '../../../../../../components/AnsibleSelect';
|
import AnsibleSelect from '../../../../../../components/AnsibleSelect';
|
||||||
import InventorySourcesList from './InventorySourcesList';
|
import InventorySourcesList from './InventorySourcesList';
|
||||||
import JobTemplatesList from './JobTemplatesList';
|
import JobTemplatesList from './JobTemplatesList';
|
||||||
import ProjectsList from './ProjectsList';
|
import ProjectsList from './ProjectsList';
|
||||||
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
|
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
|
||||||
|
import FormField from '../../../../../../components/FormField';
|
||||||
|
|
||||||
const TimeoutInput = styled(TextInput)`
|
const TimeoutInput = styled(TextInput)`
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@@ -25,19 +27,19 @@ const TimeoutLabel = styled.p`
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function NodeTypeStep({
|
function NodeTypeStep({ i18n }) {
|
||||||
description,
|
const [nodeTypeField, , nodeTypeHelpers] = useField('nodeType');
|
||||||
i18n,
|
const [nodeResourceField, , nodeResourceHelpers] = useField('nodeResource');
|
||||||
name,
|
const [, approvalNameMeta, approvalNameHelpers] = useField('approvalName');
|
||||||
nodeResource,
|
const [, , approvalDescriptionHelpers] = useField('approvalDescription');
|
||||||
nodeType,
|
const [timeoutMinutesField, , timeoutMinutesHelpers] = useField(
|
||||||
timeout,
|
'timeoutMinutes'
|
||||||
onUpdateDescription,
|
);
|
||||||
onUpdateName,
|
const [timeoutSecondsField, , timeoutSecondsHelpers] = useField(
|
||||||
onUpdateNodeResource,
|
'timeoutSeconds'
|
||||||
onUpdateNodeType,
|
);
|
||||||
onUpdateTimeout,
|
|
||||||
}) {
|
const isValid = !approvalNameMeta.touched || !approvalNameMeta.error;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div css="display: flex; align-items: center; margin-bottom: 20px;">
|
<div css="display: flex; align-items: center; margin-bottom: 20px;">
|
||||||
@@ -48,14 +50,14 @@ function NodeTypeStep({
|
|||||||
label={i18n._(t`Select a Node Type`)}
|
label={i18n._(t`Select a Node Type`)}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
key: 'approval',
|
key: 'workflow_approval_template',
|
||||||
value: 'approval',
|
value: 'workflow_approval_template',
|
||||||
label: i18n._(t`Approval`),
|
label: i18n._(t`Approval`),
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'inventory_source_sync',
|
key: 'inventory_source',
|
||||||
value: 'inventory_source_sync',
|
value: 'inventory_source',
|
||||||
label: i18n._(t`Inventory Source Sync`),
|
label: i18n._(t`Inventory Source Sync`),
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
@@ -66,8 +68,8 @@ function NodeTypeStep({
|
|||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'project_sync',
|
key: 'project',
|
||||||
value: 'project_sync',
|
value: 'project',
|
||||||
label: i18n._(t`Project Sync`),
|
label: i18n._(t`Project Sync`),
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
@@ -78,189 +80,98 @@ function NodeTypeStep({
|
|||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={nodeType}
|
value={nodeTypeField.value}
|
||||||
onChange={(e, val) => {
|
onChange={(e, val) => {
|
||||||
onUpdateNodeType(val);
|
nodeTypeHelpers.setValue(val);
|
||||||
|
nodeResourceHelpers.setValue(null);
|
||||||
|
approvalNameHelpers.setValue('');
|
||||||
|
approvalDescriptionHelpers.setValue('');
|
||||||
|
timeoutMinutesHelpers.setValue(0);
|
||||||
|
timeoutSecondsHelpers.setValue(0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{nodeType === 'job_template' && (
|
{nodeTypeField.value === 'job_template' && (
|
||||||
<JobTemplatesList
|
<JobTemplatesList
|
||||||
nodeResource={nodeResource}
|
nodeResource={nodeResourceField.value}
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{nodeType === 'project_sync' && (
|
{nodeTypeField.value === 'project' && (
|
||||||
<ProjectsList
|
<ProjectsList
|
||||||
nodeResource={nodeResource}
|
nodeResource={nodeResourceField.value}
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{nodeType === 'inventory_source_sync' && (
|
{nodeTypeField.value === 'inventory_source' && (
|
||||||
<InventorySourcesList
|
<InventorySourcesList
|
||||||
nodeResource={nodeResource}
|
nodeResource={nodeResourceField.value}
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{nodeType === 'workflow_job_template' && (
|
{nodeTypeField.value === 'workflow_job_template' && (
|
||||||
<WorkflowJobTemplatesList
|
<WorkflowJobTemplatesList
|
||||||
nodeResource={nodeResource}
|
nodeResource={nodeResourceField.value}
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{nodeType === 'approval' && (
|
{nodeTypeField.value === 'workflow_approval_template' && (
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
name: name || '',
|
|
||||||
description: description || '',
|
|
||||||
timeoutMinutes: Math.floor(timeout / 60),
|
|
||||||
timeoutSeconds: timeout - Math.floor(timeout / 60) * 60,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{() => (
|
|
||||||
<Form css="margin-top: 20px;">
|
<Form css="margin-top: 20px;">
|
||||||
<FormFullWidthLayout>
|
<FormFullWidthLayout>
|
||||||
<Field name="name">
|
<FormField
|
||||||
{({ field, form }) => {
|
name="approvalName"
|
||||||
const isValid =
|
|
||||||
form &&
|
|
||||||
(!form.touched[field.name] || !form.errors[field.name]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup
|
|
||||||
fieldId="approval-name"
|
|
||||||
isRequired
|
|
||||||
validated={isValid ? 'default' : 'error'}
|
|
||||||
label={i18n._(t`Name`)}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
autoFocus
|
|
||||||
id="approval-name"
|
id="approval-name"
|
||||||
isRequired
|
isRequired
|
||||||
|
validate={required(null, i18n)}
|
||||||
validated={isValid ? 'default' : 'error'}
|
validated={isValid ? 'default' : 'error'}
|
||||||
type="text"
|
label={i18n._(t`Name`)}
|
||||||
{...field}
|
|
||||||
onChange={(value, evt) => {
|
|
||||||
onUpdateName(evt.target.value);
|
|
||||||
field.onChange(evt);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
<FormField
|
||||||
);
|
name="approvalDescription"
|
||||||
}}
|
|
||||||
</Field>
|
|
||||||
<Field name="description">
|
|
||||||
{({ field }) => (
|
|
||||||
<FormGroup
|
|
||||||
fieldId="approval-description"
|
|
||||||
label={i18n._(t`Description`)}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
id="approval-description"
|
id="approval-description"
|
||||||
type="text"
|
label={i18n._(t`Description`)}
|
||||||
{...field}
|
|
||||||
onChange={(value, evt) => {
|
|
||||||
onUpdateDescription(evt.target.value);
|
|
||||||
field.onChange(evt);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={i18n._(t`Timeout`)}
|
label={i18n._(t`Timeout`)}
|
||||||
fieldId="approval-timeout"
|
fieldId="approval-timeout"
|
||||||
|
name="timeout"
|
||||||
>
|
>
|
||||||
<div css="display: flex;align-items: center;">
|
<div css="display: flex;align-items: center;">
|
||||||
<Field name="timeoutMinutes">
|
|
||||||
{({ field, form }) => (
|
|
||||||
<>
|
|
||||||
<TimeoutInput
|
<TimeoutInput
|
||||||
|
{...timeoutMinutesField}
|
||||||
|
aria-label={i18n._(t`Timeout minutes`)}
|
||||||
id="approval-timeout-minutes"
|
id="approval-timeout-minutes"
|
||||||
type="number"
|
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
onChange={(value, event) => {
|
||||||
{...field}
|
timeoutMinutesField.onChange(event);
|
||||||
onChange={(value, evt) => {
|
|
||||||
if (
|
|
||||||
!evt.target.value ||
|
|
||||||
evt.target.value === ''
|
|
||||||
) {
|
|
||||||
evt.target.value = 0;
|
|
||||||
}
|
|
||||||
onUpdateTimeout(
|
|
||||||
Number(evt.target.value) * 60 +
|
|
||||||
Number(form.values.timeoutSeconds)
|
|
||||||
);
|
|
||||||
field.onChange(evt);
|
|
||||||
}}
|
}}
|
||||||
|
step="1"
|
||||||
|
type="number"
|
||||||
/>
|
/>
|
||||||
<TimeoutLabel>
|
<TimeoutLabel>
|
||||||
<Trans>min</Trans>
|
<Trans>min</Trans>
|
||||||
</TimeoutLabel>
|
</TimeoutLabel>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="timeoutSeconds">
|
|
||||||
{({ field, form }) => (
|
|
||||||
<>
|
|
||||||
<TimeoutInput
|
<TimeoutInput
|
||||||
|
{...timeoutSecondsField}
|
||||||
|
aria-label={i18n._(t`Timeout seconds`)}
|
||||||
id="approval-timeout-seconds"
|
id="approval-timeout-seconds"
|
||||||
type="number"
|
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
onChange={(value, event) => {
|
||||||
{...field}
|
timeoutSecondsField.onChange(event);
|
||||||
onChange={(value, evt) => {
|
|
||||||
if (
|
|
||||||
!evt.target.value ||
|
|
||||||
evt.target.value === ''
|
|
||||||
) {
|
|
||||||
evt.target.value = 0;
|
|
||||||
}
|
|
||||||
onUpdateTimeout(
|
|
||||||
Number(evt.target.value) +
|
|
||||||
Number(form.values.timeoutMinutes) * 60
|
|
||||||
);
|
|
||||||
field.onChange(evt);
|
|
||||||
}}
|
}}
|
||||||
|
step="1"
|
||||||
|
type="number"
|
||||||
/>
|
/>
|
||||||
<TimeoutLabel>
|
<TimeoutLabel>
|
||||||
<Trans>sec</Trans>
|
<Trans>sec</Trans>
|
||||||
</TimeoutLabel>
|
</TimeoutLabel>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FormFullWidthLayout>
|
</FormFullWidthLayout>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeTypeStep.propTypes = {
|
|
||||||
description: string,
|
|
||||||
name: string,
|
|
||||||
nodeResource: shape(),
|
|
||||||
nodeType: string,
|
|
||||||
timeout: number,
|
|
||||||
onUpdateDescription: func.isRequired,
|
|
||||||
onUpdateName: func.isRequired,
|
|
||||||
onUpdateNodeResource: func.isRequired,
|
|
||||||
onUpdateNodeType: func.isRequired,
|
|
||||||
onUpdateTimeout: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
NodeTypeStep.defaultProps = {
|
|
||||||
description: '',
|
|
||||||
name: '',
|
|
||||||
nodeResource: null,
|
|
||||||
nodeType: 'job_template',
|
|
||||||
timeout: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(NodeTypeStep);
|
export default withI18n()(NodeTypeStep);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { Formik } from 'formik';
|
||||||
import { mountWithContexts } from '../../../../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../../../../testUtils/enzymeHelpers';
|
||||||
import {
|
import {
|
||||||
InventorySourcesAPI,
|
InventorySourcesAPI,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
ProjectsAPI,
|
ProjectsAPI,
|
||||||
WorkflowJobTemplatesAPI,
|
WorkflowJobTemplatesAPI,
|
||||||
} from '../../../../../../api';
|
} from '../../../../../../api';
|
||||||
|
|
||||||
import NodeTypeStep from './NodeTypeStep';
|
import NodeTypeStep from './NodeTypeStep';
|
||||||
|
|
||||||
jest.mock('../../../../../../api/models/InventorySources');
|
jest.mock('../../../../../../api/models/InventorySources');
|
||||||
@@ -14,12 +16,6 @@ jest.mock('../../../../../../api/models/JobTemplates');
|
|||||||
jest.mock('../../../../../../api/models/Projects');
|
jest.mock('../../../../../../api/models/Projects');
|
||||||
jest.mock('../../../../../../api/models/WorkflowJobTemplates');
|
jest.mock('../../../../../../api/models/WorkflowJobTemplates');
|
||||||
|
|
||||||
const onUpdateDescription = jest.fn();
|
|
||||||
const onUpdateName = jest.fn();
|
|
||||||
const onUpdateNodeResource = jest.fn();
|
|
||||||
const onUpdateNodeType = jest.fn();
|
|
||||||
const onUpdateTimeout = jest.fn();
|
|
||||||
|
|
||||||
describe('NodeTypeStep', () => {
|
describe('NodeTypeStep', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
JobTemplatesAPI.read.mockResolvedValue({
|
JobTemplatesAPI.read.mockResolvedValue({
|
||||||
@@ -118,90 +114,50 @@ describe('NodeTypeStep', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<NodeTypeStep
|
<Formik initialValues={{ nodeType: 'job_template' }}>
|
||||||
onUpdateDescription={onUpdateDescription}
|
<NodeTypeStep />
|
||||||
onUpdateName={onUpdateName}
|
</Formik>
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
|
||||||
onUpdateNodeType={onUpdateNodeType}
|
|
||||||
onUpdateTimeout={onUpdateTimeout}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('job_template');
|
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('job_template');
|
||||||
expect(wrapper.find('JobTemplatesList').length).toBe(1);
|
expect(wrapper.find('JobTemplatesList').length).toBe(1);
|
||||||
wrapper.find('Radio').simulate('click');
|
|
||||||
expect(onUpdateNodeResource).toHaveBeenCalledWith({
|
|
||||||
id: 1,
|
|
||||||
name: 'Test Job Template',
|
|
||||||
type: 'job_template',
|
|
||||||
url: '/api/v2/job_templates/1',
|
|
||||||
});
|
});
|
||||||
});
|
test('It shows the project list when node type is project', async () => {
|
||||||
test('It shows the project list when node type is project sync', async () => {
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<NodeTypeStep
|
<Formik initialValues={{ nodeType: 'project' }}>
|
||||||
nodeType="project_sync"
|
<NodeTypeStep />
|
||||||
onUpdateDescription={onUpdateDescription}
|
</Formik>
|
||||||
onUpdateName={onUpdateName}
|
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
|
||||||
onUpdateNodeType={onUpdateNodeType}
|
|
||||||
onUpdateTimeout={onUpdateTimeout}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
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);
|
expect(wrapper.find('ProjectsList').length).toBe(1);
|
||||||
wrapper.find('Radio').simulate('click');
|
|
||||||
expect(onUpdateNodeResource).toHaveBeenCalledWith({
|
|
||||||
id: 1,
|
|
||||||
name: 'Test Project',
|
|
||||||
type: 'project',
|
|
||||||
url: '/api/v2/projects/1',
|
|
||||||
});
|
});
|
||||||
});
|
test('It shows the inventory source list when node type is inventory source', async () => {
|
||||||
test('It shows the inventory source list when node type is inventory source sync', async () => {
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<NodeTypeStep
|
<Formik initialValues={{ nodeType: 'inventory_source' }}>
|
||||||
nodeType="inventory_source_sync"
|
<NodeTypeStep />
|
||||||
onUpdateDescription={onUpdateDescription}
|
</Formik>
|
||||||
onUpdateName={onUpdateName}
|
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
|
||||||
onUpdateNodeType={onUpdateNodeType}
|
|
||||||
onUpdateTimeout={onUpdateTimeout}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
|
expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||||
'inventory_source_sync'
|
'inventory_source'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('InventorySourcesList').length).toBe(1);
|
expect(wrapper.find('InventorySourcesList').length).toBe(1);
|
||||||
wrapper.find('Radio').simulate('click');
|
|
||||||
expect(onUpdateNodeResource).toHaveBeenCalledWith({
|
|
||||||
id: 1,
|
|
||||||
name: 'Test Inventory Source',
|
|
||||||
type: 'inventory_source',
|
|
||||||
url: '/api/v2/inventory_sources/1',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
test('It shows the workflow job template list when node type is workflow job template', async () => {
|
test('It shows the workflow job template list when node type is workflow job template', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<NodeTypeStep
|
<Formik initialValues={{ nodeType: 'workflow_job_template' }}>
|
||||||
nodeType="workflow_job_template"
|
<NodeTypeStep />
|
||||||
onUpdateDescription={onUpdateDescription}
|
</Formik>
|
||||||
onUpdateName={onUpdateName}
|
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
|
||||||
onUpdateNodeType={onUpdateNodeType}
|
|
||||||
onUpdateTimeout={onUpdateTimeout}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -209,67 +165,60 @@ describe('NodeTypeStep', () => {
|
|||||||
'workflow_job_template'
|
'workflow_job_template'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('WorkflowJobTemplatesList').length).toBe(1);
|
expect(wrapper.find('WorkflowJobTemplatesList').length).toBe(1);
|
||||||
wrapper.find('Radio').simulate('click');
|
|
||||||
expect(onUpdateNodeResource).toHaveBeenCalledWith({
|
|
||||||
id: 1,
|
|
||||||
name: 'Test Workflow Job Template',
|
|
||||||
type: 'workflow_job_template',
|
|
||||||
url: '/api/v2/workflow_job_templates/1',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
test('It shows the approval form fields when node type is approval', async () => {
|
test('It shows the approval form fields when node type is approval', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<NodeTypeStep
|
<Formik
|
||||||
nodeType="approval"
|
initialValues={{
|
||||||
onUpdateDescription={onUpdateDescription}
|
nodeType: 'workflow_approval_template',
|
||||||
onUpdateName={onUpdateName}
|
approvalName: '',
|
||||||
onUpdateNodeResource={onUpdateNodeResource}
|
approvalDescription: '',
|
||||||
onUpdateNodeType={onUpdateNodeType}
|
timeoutMinutes: 0,
|
||||||
onUpdateTimeout={onUpdateTimeout}
|
timeoutSeconds: 0,
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<NodeTypeStep />
|
||||||
|
</Formik>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('approval');
|
expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
|
||||||
expect(wrapper.find('input#approval-name').length).toBe(1);
|
'workflow_approval_template'
|
||||||
expect(wrapper.find('input#approval-description').length).toBe(1);
|
);
|
||||||
expect(wrapper.find('input#approval-timeout-minutes').length).toBe(1);
|
expect(wrapper.find('FormField[label="Name"]').length).toBe(1);
|
||||||
expect(wrapper.find('input#approval-timeout-seconds').length).toBe(1);
|
expect(wrapper.find('FormField[label="Description"]').length).toBe(1);
|
||||||
|
expect(wrapper.find('input[name="timeoutMinutes"]').length).toBe(1);
|
||||||
|
expect(wrapper.find('input[name="timeoutSeconds"]').length).toBe(1);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('input#approval-name').simulate('change', {
|
wrapper.find('input#approval-name').simulate('change', {
|
||||||
target: { value: 'Test Approval', name: 'name' },
|
target: { value: 'Test Approval', name: 'approvalName' },
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
expect(onUpdateName).toHaveBeenCalledWith('Test Approval');
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
wrapper.find('input#approval-description').simulate('change', {
|
wrapper.find('input#approval-description').simulate('change', {
|
||||||
target: { value: 'Test Approval Description', name: 'description' },
|
target: {
|
||||||
|
value: 'Test Approval Description',
|
||||||
|
name: 'approvalDescription',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
wrapper.find('input[name="timeoutMinutes"]').simulate('change', {
|
||||||
|
|
||||||
expect(onUpdateDescription).toHaveBeenCalledWith(
|
|
||||||
'Test Approval Description'
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
wrapper.find('input#approval-timeout-minutes').simulate('change', {
|
|
||||||
target: { value: 5, name: 'timeoutMinutes' },
|
target: { value: 5, name: 'timeoutMinutes' },
|
||||||
});
|
});
|
||||||
});
|
wrapper.find('input[name="timeoutSeconds"]').simulate('change', {
|
||||||
|
|
||||||
expect(onUpdateTimeout).toHaveBeenCalledWith(300);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
wrapper.find('input#approval-timeout-seconds').simulate('change', {
|
|
||||||
target: { value: 30, name: 'timeoutSeconds' },
|
target: { value: 30, name: 'timeoutSeconds' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onUpdateTimeout).toHaveBeenCalledWith(330);
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('input#approval-name').prop('value')).toBe(
|
||||||
|
'Test Approval'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('input#approval-description').prop('value')).toBe(
|
||||||
|
'Test Approval Description'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('input[name="timeoutMinutes"]').prop('value')).toBe(5);
|
||||||
|
expect(wrapper.find('input[name="timeoutSeconds"]').prop('value')).toBe(30);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import NodeTypeStep from './NodeTypeStep';
|
||||||
|
import StepName from '../../../../../../components/LaunchPrompt/steps/StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'nodeType';
|
||||||
|
|
||||||
|
export default function useNodeTypeStep(i18n) {
|
||||||
|
const [, meta] = useField('nodeType');
|
||||||
|
const [approvalNameField] = useField('approvalName');
|
||||||
|
const [nodeTypeField, ,] = useField('nodeType');
|
||||||
|
const [nodeResourceField] = useField('nodeResource');
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField),
|
||||||
|
initialValues: getInitialValues(),
|
||||||
|
isReady: true,
|
||||||
|
contentError: null,
|
||||||
|
formError: meta.error,
|
||||||
|
setTouched: setFieldsTouched => {
|
||||||
|
setFieldsTouched({
|
||||||
|
inventory: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||||
|
const isEnabled = () => {
|
||||||
|
if (
|
||||||
|
(nodeTypeField.value !== 'workflow_approval_template' &&
|
||||||
|
nodeResourceField.value === null) ||
|
||||||
|
(nodeTypeField.value === 'workflow_approval_template' &&
|
||||||
|
approvalNameField.value === undefined)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={false} id="node-type-step">
|
||||||
|
{i18n._(t`Node type`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: <NodeTypeStep i18n={i18n} />,
|
||||||
|
enableNext: isEnabled(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ContentLoading from '../../../../../components/ContentLoading';
|
||||||
import PromptDetail from '../../../../../components/PromptDetail';
|
import PromptDetail from '../../../../../components/PromptDetail';
|
||||||
import useRequest from '../../../../../util/useRequest';
|
import useRequest from '../../../../../util/useRequest';
|
||||||
import {
|
import { jsonToYaml } from '../../../../../util/yaml';
|
||||||
InventorySourcesAPI,
|
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../../../api';
|
||||||
JobTemplatesAPI,
|
import getNodeType from '../../shared/WorkflowJobTemplateVisualizerUtils';
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function NodeViewModal({ i18n, readOnly }) {
|
function NodeViewModal({ i18n, readOnly }) {
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const { nodeToView } = useContext(WorkflowStateContext);
|
const { nodeToView } = useContext(WorkflowStateContext);
|
||||||
const { unifiedJobTemplate } = nodeToView;
|
const {
|
||||||
const [nodeType, nodeAPI] = getNodeType(unifiedJobTemplate);
|
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 {
|
const {
|
||||||
result: launchConfig,
|
result: launchConfig,
|
||||||
@@ -56,39 +42,44 @@ function NodeViewModal({ i18n, readOnly }) {
|
|||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const readLaunch =
|
const readLaunch =
|
||||||
nodeType === 'workflow_job_template'
|
nodeType === 'workflow_job_template'
|
||||||
? WorkflowJobTemplatesAPI.readLaunch(unifiedJobTemplate.id)
|
? WorkflowJobTemplatesAPI.readLaunch(id)
|
||||||
: JobTemplatesAPI.readLaunch(unifiedJobTemplate.id);
|
: JobTemplatesAPI.readLaunch(id);
|
||||||
const { data } = await readLaunch;
|
const { data } = await readLaunch;
|
||||||
return data;
|
return data;
|
||||||
}, [nodeType, unifiedJobTemplate.id]),
|
}, [nodeType, id]),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: nodeDetail,
|
result: relatedData,
|
||||||
isLoading: isNodeDetailLoading,
|
isLoading: isRelatedDataLoading,
|
||||||
error: nodeDetailError,
|
error: relatedDataError,
|
||||||
request: fetchNodeDetail,
|
request: fetchRelatedData,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
let { data } = await nodeAPI?.readDetail(unifiedJobTemplate.id);
|
const related = {};
|
||||||
|
if (
|
||||||
if (data?.type === 'job_template') {
|
nodeType === 'job_template' &&
|
||||||
|
!fullUnifiedJobTemplate.instance_groups
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
data: { results = [] },
|
data: { results = [] },
|
||||||
} = await JobTemplatesAPI.readInstanceGroups(data.id);
|
} = await JobTemplatesAPI.readInstanceGroups(fullUnifiedJobTemplate.id);
|
||||||
data = Object.assign(data, { instance_groups: results });
|
related.instance_groups = results;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.related?.webhook_receiver) {
|
if (
|
||||||
|
fullUnifiedJobTemplate?.related?.webhook_receiver &&
|
||||||
|
!fullUnifiedJobTemplate.webhook_key
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
data: { webhook_key },
|
data: { webhook_key },
|
||||||
} = await nodeAPI?.readWebhookKey(data.id);
|
} = await nodeAPI?.readWebhookKey(fullUnifiedJobTemplate.id);
|
||||||
data = Object.assign(data, { webhook_key });
|
related.webhook_key = webhook_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return related;
|
||||||
}, [nodeAPI, unifiedJobTemplate.id]),
|
}, [nodeAPI, fullUnifiedJobTemplate, nodeType]),
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,21 +88,27 @@ function NodeViewModal({ i18n, readOnly }) {
|
|||||||
fetchLaunchConfig();
|
fetchLaunchConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unifiedJobTemplate.unified_job_type && nodeType !== 'approval') {
|
if (
|
||||||
fetchNodeDetail();
|
fullUnifiedJobTemplate &&
|
||||||
|
((nodeType === 'job_template' &&
|
||||||
|
!fullUnifiedJobTemplate.instance_groups) ||
|
||||||
|
(fullUnifiedJobTemplate?.related?.webhook_receiver &&
|
||||||
|
!fullUnifiedJobTemplate.webhook_key))
|
||||||
|
) {
|
||||||
|
fetchRelatedData();
|
||||||
}
|
}
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodeDetail) {
|
if (relatedData) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'REFRESH_NODE',
|
type: 'REFRESH_NODE',
|
||||||
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 = () => {
|
const handleEdit = () => {
|
||||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: null });
|
dispatch({ type: 'SET_NODE_TO_VIEW', value: null });
|
||||||
@@ -119,13 +116,74 @@ function NodeViewModal({ i18n, readOnly }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let Content;
|
let Content;
|
||||||
if (isLaunchConfigLoading || isNodeDetailLoading) {
|
if (isLaunchConfigLoading || isRelatedDataLoading) {
|
||||||
Content = <ContentLoading />;
|
Content = <ContentLoading />;
|
||||||
} else if (launchConfigError || nodeDetailError) {
|
} else if (launchConfigError || relatedDataError) {
|
||||||
Content = <ContentError error={launchConfigError || nodeDetailError} />;
|
Content = <ContentError error={launchConfigError || relatedDataError} />;
|
||||||
} else {
|
} else if (!fullUnifiedJobTemplate) {
|
||||||
Content = (
|
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
|
<Modal
|
||||||
variant="large"
|
variant="large"
|
||||||
isOpen
|
isOpen
|
||||||
title={unifiedJobTemplate.name}
|
title={fullUnifiedJobTemplate?.name || i18n._(t`Resource deleted`)}
|
||||||
aria-label={i18n._(t`Workflow node view modal`)}
|
aria-label={i18n._(t`Workflow node view modal`)}
|
||||||
onClose={() => dispatch({ type: 'SET_NODE_TO_VIEW', value: null })}
|
onClose={() => dispatch({ type: 'SET_NODE_TO_VIEW', value: null })}
|
||||||
actions={
|
actions={
|
||||||
|
|||||||
@@ -53,13 +53,16 @@ describe('NodeViewModal', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
const workflowContext = {
|
const workflowContext = {
|
||||||
nodeToView: {
|
nodeToView: {
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Mock Node',
|
name: 'Mock Node',
|
||||||
description: '',
|
description: '',
|
||||||
unified_job_type: 'workflow_job',
|
unified_job_type: 'workflow_job',
|
||||||
created: '2019-08-08T19:24:05.344276Z',
|
created: '2019-08-08T19:24:05.344276Z',
|
||||||
modified: '2019-08-08T19:24:18.162949Z',
|
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', () => {
|
test('should fetch workflow template launch data', () => {
|
||||||
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||||
expect(JobTemplatesAPI.readDetail).not.toHaveBeenCalled();
|
|
||||||
expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
|
expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
|
||||||
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||||
expect(WorkflowJobTemplatesAPI.readWebhookKey).toHaveBeenCalledWith(1);
|
expect(WorkflowJobTemplatesAPI.readWebhookKey).toHaveBeenCalledWith(1);
|
||||||
@@ -118,7 +120,7 @@ describe('NodeViewModal', () => {
|
|||||||
describe('Job template node', () => {
|
describe('Job template node', () => {
|
||||||
const workflowContext = {
|
const workflowContext = {
|
||||||
nodeToView: {
|
nodeToView: {
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Mock Node',
|
name: 'Mock Node',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -145,7 +147,6 @@ describe('NodeViewModal', () => {
|
|||||||
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
|
||||||
expect(JobTemplatesAPI.readWebhookKey).not.toHaveBeenCalledWith();
|
expect(JobTemplatesAPI.readWebhookKey).not.toHaveBeenCalledWith();
|
||||||
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||||
expect(JobTemplatesAPI.readDetail).toHaveBeenCalledWith(1);
|
|
||||||
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -207,7 +208,7 @@ describe('NodeViewModal', () => {
|
|||||||
describe('Project node', () => {
|
describe('Project node', () => {
|
||||||
const workflowContext = {
|
const workflowContext = {
|
||||||
nodeToView: {
|
nodeToView: {
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Mock Node',
|
name: 'Mock Node',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -237,4 +238,71 @@ describe('NodeViewModal', () => {
|
|||||||
jest.clearAllMocks();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { useField } from 'formik';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { func, string } from 'prop-types';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
import { Title } from '@patternfly/react-core';
|
||||||
import SelectableCard from '../../../../../components/SelectableCard';
|
import SelectableCard from '../../../../../components/SelectableCard';
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ const Grid = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function RunStep({ i18n, linkType, onUpdateLinkType }) {
|
function RunStep({ i18n }) {
|
||||||
|
const [field, , helpers] = useField('linkType');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title headingLevel="h1" size="xl">
|
<Title headingLevel="h1" size="xl">
|
||||||
@@ -30,39 +31,33 @@ function RunStep({ i18n, linkType, onUpdateLinkType }) {
|
|||||||
<Grid>
|
<Grid>
|
||||||
<SelectableCard
|
<SelectableCard
|
||||||
id="link-type-success"
|
id="link-type-success"
|
||||||
isSelected={linkType === 'success'}
|
isSelected={field.value === 'success'}
|
||||||
label={i18n._(t`On Success`)}
|
label={i18n._(t`On Success`)}
|
||||||
description={i18n._(
|
description={i18n._(
|
||||||
t`Execute when the parent node results in a successful state.`
|
t`Execute when the parent node results in a successful state.`
|
||||||
)}
|
)}
|
||||||
onClick={() => onUpdateLinkType('success')}
|
onClick={() => helpers.setValue('success')}
|
||||||
/>
|
/>
|
||||||
<SelectableCard
|
<SelectableCard
|
||||||
id="link-type-failure"
|
id="link-type-failure"
|
||||||
isSelected={linkType === 'failure'}
|
isSelected={field.value === 'failure'}
|
||||||
label={i18n._(t`On Failure`)}
|
label={i18n._(t`On Failure`)}
|
||||||
description={i18n._(
|
description={i18n._(
|
||||||
t`Execute when the parent node results in a failure state.`
|
t`Execute when the parent node results in a failure state.`
|
||||||
)}
|
)}
|
||||||
onClick={() => onUpdateLinkType('failure')}
|
onClick={() => helpers.setValue('failure')}
|
||||||
/>
|
/>
|
||||||
<SelectableCard
|
<SelectableCard
|
||||||
id="link-type-always"
|
id="link-type-always"
|
||||||
isSelected={linkType === 'always'}
|
isSelected={field.value === 'always'}
|
||||||
label={i18n._(t`Always`)}
|
label={i18n._(t`Always`)}
|
||||||
description={i18n._(
|
description={i18n._(
|
||||||
t`Execute regardless of the parent node's final state.`
|
t`Execute regardless of the parent node's final state.`
|
||||||
)}
|
)}
|
||||||
onClick={() => onUpdateLinkType('always')}
|
onClick={() => helpers.setValue('always')}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RunStep.propTypes = {
|
|
||||||
linkType: string.isRequired,
|
|
||||||
onUpdateLinkType: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(RunStep);
|
export default withI18n()(RunStep);
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
||||||
import RunStep from './RunStep';
|
import RunStep from './RunStep';
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const linkType = 'always';
|
|
||||||
const onUpdateLinkType = jest.fn();
|
|
||||||
|
|
||||||
describe('RunStep', () => {
|
describe('RunStep', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<RunStep linkType={linkType} onUpdateLinkType={onUpdateLinkType} />
|
<Formik initialValues={{ linkType: 'success' }}>
|
||||||
|
<RunStep />
|
||||||
|
</Formik>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,23 +21,20 @@ describe('RunStep', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Default selected card matches default link type when present', () => {
|
test('Default selected card matches default link type when present', () => {
|
||||||
expect(wrapper.find('#link-type-success').props().isSelected).toBe(false);
|
expect(wrapper.find('#link-type-success').props().isSelected).toBe(true);
|
||||||
expect(wrapper.find('#link-type-failure').props().isSelected).toBe(false);
|
expect(wrapper.find('#link-type-failure').props().isSelected).toBe(false);
|
||||||
|
expect(wrapper.find('#link-type-always').props().isSelected).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Clicking success card makes expected callback', async () => {
|
||||||
|
await act(async () => wrapper.find('#link-type-always').simulate('click'));
|
||||||
|
wrapper.update();
|
||||||
expect(wrapper.find('#link-type-always').props().isSelected).toBe(true);
|
expect(wrapper.find('#link-type-always').props().isSelected).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Clicking success card makes expected callback', () => {
|
test('Clicking failure card makes expected callback', async () => {
|
||||||
wrapper.find('#link-type-success').simulate('click');
|
await act(async () => wrapper.find('#link-type-failure').simulate('click'));
|
||||||
expect(onUpdateLinkType).toHaveBeenCalledWith('success');
|
wrapper.update();
|
||||||
});
|
expect(wrapper.find('#link-type-failure').props().isSelected).toBe(true);
|
||||||
|
|
||||||
test('Clicking failure card makes expected callback', () => {
|
|
||||||
wrapper.find('#link-type-failure').simulate('click');
|
|
||||||
expect(onUpdateLinkType).toHaveBeenCalledWith('failure');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Clicking always card makes expected callback', () => {
|
|
||||||
wrapper.find('#link-type-always').simulate('click');
|
|
||||||
expect(onUpdateLinkType).toHaveBeenCalledWith('always');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import RunStep from './RunStep';
|
||||||
|
import StepName from '../../../../../components/LaunchPrompt/steps/StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'runType';
|
||||||
|
|
||||||
|
export default function useRunTypeStep(i18n, askLinkType) {
|
||||||
|
const [, meta] = useField('linkType');
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: getStep(askLinkType, meta, i18n),
|
||||||
|
initialValues: askLinkType ? { linkType: 'success' } : {},
|
||||||
|
isReady: true,
|
||||||
|
contentError: null,
|
||||||
|
formError: meta.error,
|
||||||
|
setTouched: setFieldsTouched => {
|
||||||
|
setFieldsTouched({
|
||||||
|
inventory: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getStep(askLinkType, meta, i18n) {
|
||||||
|
if (!askLinkType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={false} id="run-type-step">
|
||||||
|
{i18n._(t`Run type`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: <RunStep />,
|
||||||
|
enableNext: meta.value !== '',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
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) {
|
||||||
|
const newExtraData = { ...sourceOfValues.extra_data };
|
||||||
|
if (launchConfig.survey_enabled && 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(
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
i18n,
|
||||||
|
resource,
|
||||||
|
askLinkType
|
||||||
|
) {
|
||||||
|
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||||
|
const { resetForm, values: formikValues } = useFormikContext();
|
||||||
|
const [visited, setVisited] = useState({});
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
useRunTypeStep(i18n, askLinkType),
|
||||||
|
useNodeTypeStep(i18n),
|
||||||
|
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||||
|
useCredentialsStep(launchConfig, resource, i18n),
|
||||||
|
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||||
|
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasErrors = steps.some(step => step.formError);
|
||||||
|
steps.push(
|
||||||
|
usePreviewStep(
|
||||||
|
launchConfig,
|
||||||
|
i18n,
|
||||||
|
resource,
|
||||||
|
surveyConfig,
|
||||||
|
hasErrors,
|
||||||
|
showPreviewStep(formikValues.nodeType, launchConfig)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
||||||
|
const isReady = !steps.some(s => !s.isReady);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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,
|
||||||
|
nodeType: formikValues.nodeType,
|
||||||
|
linkType: formikValues.linkType,
|
||||||
|
verbosity: initialValues?.verbosity?.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [launchConfig, surveyConfig, isReady]);
|
||||||
|
|
||||||
|
const stepWithError = steps.find(s => s.contentError);
|
||||||
|
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
steps: pfSteps,
|
||||||
|
visitStep: stepId =>
|
||||||
|
setVisited({
|
||||||
|
...visited,
|
||||||
|
[stepId]: true,
|
||||||
|
}),
|
||||||
|
visitAllSteps: setFieldsTouched => {
|
||||||
|
setVisited({
|
||||||
|
inventory: true,
|
||||||
|
credentials: true,
|
||||||
|
other: true,
|
||||||
|
survey: true,
|
||||||
|
preview: true,
|
||||||
|
});
|
||||||
|
steps.forEach(s => s.setTouched(setFieldsTouched));
|
||||||
|
},
|
||||||
|
contentError,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import React, { useEffect, useReducer } from 'react';
|
import React, { useCallback, useEffect, useReducer } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { shape } from 'prop-types';
|
import { shape } from 'prop-types';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
WorkflowDispatchContext,
|
WorkflowDispatchContext,
|
||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
} from '../../../contexts/Workflow';
|
} from '../../../contexts/Workflow';
|
||||||
|
import { getAddedAndRemoved } from '../../../util/lists';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import { layoutGraph } from '../../../components/Workflow/WorkflowUtils';
|
import { layoutGraph } from '../../../components/Workflow/WorkflowUtils';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
import ContentLoading from '../../../components/ContentLoading';
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
import workflowReducer from '../../../components/Workflow/workflowReducer';
|
import workflowReducer from '../../../components/Workflow/workflowReducer';
|
||||||
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { DeleteAllNodesModal, UnsavedChangesModal } from './Modals';
|
import { DeleteAllNodesModal, UnsavedChangesModal } from './Modals';
|
||||||
import {
|
import {
|
||||||
LinkAddModal,
|
LinkAddModal,
|
||||||
@@ -46,6 +51,45 @@ const Wrapper = styled.div`
|
|||||||
height: 100%;
|
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 (
|
const fetchWorkflowNodes = async (
|
||||||
templateId,
|
templateId,
|
||||||
pageNo = 1,
|
pageNo = 1,
|
||||||
@@ -206,7 +250,49 @@ function Visualizer({ template, i18n }) {
|
|||||||
return disassociateNodeRequests;
|
return disassociateNodeRequests;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateLinkMapAndNewLinks = originalLinkMap => {
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const workflowNodes = await fetchWorkflowNodes(template.id);
|
||||||
|
dispatch({
|
||||||
|
type: 'GENERATE_NODES_AND_LINKS',
|
||||||
|
nodes: workflowNodes,
|
||||||
|
i18n,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: 'SET_CONTENT_ERROR', value: error });
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_IS_LOADING', value: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, [template.id, i18n]);
|
||||||
|
|
||||||
|
// Update positions of nodes/links
|
||||||
|
useEffect(() => {
|
||||||
|
if (nodes) {
|
||||||
|
const newNodePositions = {};
|
||||||
|
const nonDeletedNodes = nodes.filter(node => !node.isDeleted);
|
||||||
|
const g = layoutGraph(nonDeletedNodes, links);
|
||||||
|
|
||||||
|
g.nodes().forEach(node => {
|
||||||
|
newNodePositions[node] = g.node(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_NODE_POSITIONS', value: newNodePositions });
|
||||||
|
}
|
||||||
|
}, [links, nodes]);
|
||||||
|
|
||||||
|
const { error: saveVisualizerError, request: saveVisualizer } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const nodeRequests = [];
|
||||||
|
const approvalTemplateRequests = [];
|
||||||
|
const originalLinkMap = {};
|
||||||
|
const deletedNodeIds = [];
|
||||||
|
const associateCredentialRequests = [];
|
||||||
|
const disassociateCredentialRequests = [];
|
||||||
|
|
||||||
|
const generateLinkMapAndNewLinks = () => {
|
||||||
const linkMap = {};
|
const linkMap = {};
|
||||||
const newLinks = [];
|
const newLinks = [];
|
||||||
|
|
||||||
@@ -254,11 +340,6 @@ function Visualizer({ template, i18n }) {
|
|||||||
return [linkMap, newLinks];
|
return [linkMap, newLinks];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVisualizerSave = async () => {
|
|
||||||
const nodeRequests = [];
|
|
||||||
const approvalTemplateRequests = [];
|
|
||||||
const originalLinkMap = {};
|
|
||||||
const deletedNodeIds = [];
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
// node with id=1 is the artificial start node
|
// node with id=1 is the artificial start node
|
||||||
if (node.id === 1) {
|
if (node.id === 1) {
|
||||||
@@ -284,7 +365,9 @@ function Visualizer({ template, i18n }) {
|
|||||||
WorkflowJobTemplateNodesAPI.destroy(node.originalNodeObject.id)
|
WorkflowJobTemplateNodesAPI.destroy(node.originalNodeObject.id)
|
||||||
);
|
);
|
||||||
} else if (!node.isDeleted && !node.originalNodeObject) {
|
} else if (!node.isDeleted && !node.originalNodeObject) {
|
||||||
if (node.unifiedJobTemplate.type === 'workflow_approval_template') {
|
if (
|
||||||
|
node.fullUnifiedJobTemplate.type === 'workflow_approval_template'
|
||||||
|
) {
|
||||||
nodeRequests.push(
|
nodeRequests.push(
|
||||||
WorkflowJobTemplatesAPI.createNode(template.id, {}).then(
|
WorkflowJobTemplatesAPI.createNode(template.id, {}).then(
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
@@ -296,11 +379,14 @@ function Visualizer({ template, i18n }) {
|
|||||||
always_nodes: [],
|
always_nodes: [],
|
||||||
};
|
};
|
||||||
approvalTemplateRequests.push(
|
approvalTemplateRequests.push(
|
||||||
WorkflowJobTemplateNodesAPI.createApprovalTemplate(data.id, {
|
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
|
||||||
name: node.unifiedJobTemplate.name,
|
data.id,
|
||||||
description: node.unifiedJobTemplate.description,
|
{
|
||||||
timeout: node.unifiedJobTemplate.timeout,
|
name: node.fullUnifiedJobTemplate.name,
|
||||||
})
|
description: node.fullUnifiedJobTemplate.description,
|
||||||
|
timeout: node.fullUnifiedJobTemplate.timeout,
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -308,7 +394,9 @@ function Visualizer({ template, i18n }) {
|
|||||||
} else {
|
} else {
|
||||||
nodeRequests.push(
|
nodeRequests.push(
|
||||||
WorkflowJobTemplatesAPI.createNode(template.id, {
|
WorkflowJobTemplatesAPI.createNode(template.id, {
|
||||||
unified_job_template: node.unifiedJobTemplate.id,
|
...node.promptValues,
|
||||||
|
inventory: node.promptValues?.inventory?.id || null,
|
||||||
|
unified_job_template: node.fullUnifiedJobTemplate.id,
|
||||||
}).then(({ data }) => {
|
}).then(({ data }) => {
|
||||||
node.originalNodeObject = data;
|
node.originalNodeObject = data;
|
||||||
originalLinkMap[node.id] = {
|
originalLinkMap[node.id] = {
|
||||||
@@ -317,14 +405,32 @@ function Visualizer({ template, i18n }) {
|
|||||||
failure_nodes: [],
|
failure_nodes: [],
|
||||||
always_nodes: [],
|
always_nodes: [],
|
||||||
};
|
};
|
||||||
|
if (node.promptValues?.removedCredentials?.length > 0) {
|
||||||
|
node.promptValues.removedCredentials.forEach(cred => {
|
||||||
|
disassociateCredentialRequests.push(
|
||||||
|
WorkflowJobTemplateNodesAPI.disassociateCredentials(
|
||||||
|
data.id,
|
||||||
|
cred.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (node.promptValues?.addedCredentials?.length > 0) {
|
||||||
|
node.promptValues.addedCredentials.forEach(cred => {
|
||||||
|
associateCredentialRequests.push(
|
||||||
|
WorkflowJobTemplateNodesAPI.associateCredentials(
|
||||||
|
data.id,
|
||||||
|
cred.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (node.isEdited) {
|
} else if (node.isEdited) {
|
||||||
if (
|
if (
|
||||||
node.unifiedJobTemplate &&
|
node.fullUnifiedJobTemplate.type === 'workflow_approval_template'
|
||||||
(node.unifiedJobTemplate.unified_job_type === 'workflow_approval' ||
|
|
||||||
node.unifiedJobTemplate.type === 'workflow_approval_template')
|
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
node.originalNodeObject.summary_fields.unified_job_template
|
node.originalNodeObject.summary_fields.unified_job_template
|
||||||
@@ -332,11 +438,12 @@ function Visualizer({ template, i18n }) {
|
|||||||
) {
|
) {
|
||||||
approvalTemplateRequests.push(
|
approvalTemplateRequests.push(
|
||||||
WorkflowApprovalTemplatesAPI.update(
|
WorkflowApprovalTemplatesAPI.update(
|
||||||
node.originalNodeObject.summary_fields.unified_job_template.id,
|
node.originalNodeObject.summary_fields.unified_job_template
|
||||||
|
.id,
|
||||||
{
|
{
|
||||||
name: node.unifiedJobTemplate.name,
|
name: node.fullUnifiedJobTemplate.name,
|
||||||
description: node.unifiedJobTemplate.description,
|
description: node.fullUnifiedJobTemplate.description,
|
||||||
timeout: node.unifiedJobTemplate.timeout,
|
timeout: node.fullUnifiedJobTemplate.timeout,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -345,17 +452,51 @@ function Visualizer({ template, i18n }) {
|
|||||||
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
|
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
|
||||||
node.originalNodeObject.id,
|
node.originalNodeObject.id,
|
||||||
{
|
{
|
||||||
name: node.unifiedJobTemplate.name,
|
name: node.fullUnifiedJobTemplate.name,
|
||||||
description: node.unifiedJobTemplate.description,
|
description: node.fullUnifiedJobTemplate.description,
|
||||||
timeout: node.unifiedJobTemplate.timeout,
|
timeout: node.fullUnifiedJobTemplate.timeout,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nodeRequests.push(
|
nodeRequests.push(
|
||||||
WorkflowJobTemplateNodesAPI.update(node.originalNodeObject.id, {
|
WorkflowJobTemplateNodesAPI.replace(node.originalNodeObject.id, {
|
||||||
unified_job_template: node.unifiedJobTemplate.id,
|
...node.promptValues,
|
||||||
|
inventory: node.promptValues?.inventory?.id || null,
|
||||||
|
unified_job_template: node.fullUnifiedJobTemplate.id,
|
||||||
|
}).then(() => {
|
||||||
|
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 (removedCredentials?.length > 0) {
|
||||||
|
removedCredentials.forEach(cred =>
|
||||||
|
disassociateCredentialRequests.push(
|
||||||
|
WorkflowJobTemplateNodesAPI.disassociateCredentials(
|
||||||
|
node.originalNodeObject.id,
|
||||||
|
cred.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -372,41 +513,18 @@ function Visualizer({ template, i18n }) {
|
|||||||
);
|
);
|
||||||
await Promise.all(associateNodes(newLinks, originalLinkMap));
|
await Promise.all(associateNodes(newLinks, originalLinkMap));
|
||||||
|
|
||||||
|
await Promise.all(disassociateCredentialRequests);
|
||||||
|
await Promise.all(associateCredentialRequests);
|
||||||
|
|
||||||
history.push(`/templates/workflow_job_template/${template.id}/details`);
|
history.push(`/templates/workflow_job_template/${template.id}/details`);
|
||||||
};
|
}, [links, nodes, history, template.id]),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
async function fetchData() {
|
error: nodeRequestError,
|
||||||
try {
|
dismissError: dismissNodeRequestError,
|
||||||
const workflowNodes = await fetchWorkflowNodes(template.id);
|
} = useDismissableError(saveVisualizerError);
|
||||||
dispatch({
|
|
||||||
type: 'GENERATE_NODES_AND_LINKS',
|
|
||||||
nodes: workflowNodes,
|
|
||||||
i18n,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({ type: 'SET_CONTENT_ERROR', value: error });
|
|
||||||
} finally {
|
|
||||||
dispatch({ type: 'SET_IS_LOADING', value: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [template.id, i18n]);
|
|
||||||
|
|
||||||
// Update positions of nodes/links
|
|
||||||
useEffect(() => {
|
|
||||||
if (nodes) {
|
|
||||||
const newNodePositions = {};
|
|
||||||
const nonDeletedNodes = nodes.filter(node => !node.isDeleted);
|
|
||||||
const g = layoutGraph(nonDeletedNodes, links);
|
|
||||||
|
|
||||||
g.nodes().forEach(node => {
|
|
||||||
newNodePositions[node] = g.node(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_NODE_POSITIONS', value: newNodePositions });
|
|
||||||
}
|
|
||||||
}, [links, nodes]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -432,7 +550,7 @@ function Visualizer({ template, i18n }) {
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
<VisualizerToolbar
|
<VisualizerToolbar
|
||||||
onClose={handleVisualizerClose}
|
onClose={handleVisualizerClose}
|
||||||
onSave={handleVisualizerSave}
|
onSave={() => saveVisualizer(nodes)}
|
||||||
hasUnsavedChanges={unsavedChanges}
|
hasUnsavedChanges={unsavedChanges}
|
||||||
template={template}
|
template={template}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@@ -456,11 +574,22 @@ function Visualizer({ template, i18n }) {
|
|||||||
`/templates/workflow_job_template/${template.id}/details`
|
`/templates/workflow_job_template/${template.id}/details`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onSaveAndExit={() => handleVisualizerSave()}
|
onSaveAndExit={() => saveVisualizer(nodes)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showDeleteAllNodesModal && <DeleteAllNodesModal />}
|
{showDeleteAllNodesModal && <DeleteAllNodesModal />}
|
||||||
{nodeToView && <NodeViewModal readOnly={readOnly} />}
|
{nodeToView && <NodeViewModal readOnly={readOnly} />}
|
||||||
|
{nodeRequestError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissNodeRequestError}
|
||||||
|
>
|
||||||
|
{i18n._(t`There was an error saving the workflow.`)}
|
||||||
|
<ErrorDetail error={nodeRequestError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
</WorkflowDispatchContext.Provider>
|
</WorkflowDispatchContext.Provider>
|
||||||
</WorkflowStateContext.Provider>
|
</WorkflowStateContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const workflowContext = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
unifiedJobTemplate: {
|
fullUnifiedJobTemplate: {
|
||||||
name: 'Foo JT',
|
name: 'Foo JT',
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ import {
|
|||||||
WorkflowDispatchContext,
|
WorkflowDispatchContext,
|
||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
} from '../../../contexts/Workflow';
|
} 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 { constants as wfConstants } from '../../../components/Workflow/WorkflowUtils';
|
||||||
import {
|
import {
|
||||||
WorkflowActionTooltip,
|
WorkflowActionTooltip,
|
||||||
WorkflowActionTooltipItem,
|
WorkflowActionTooltipItem,
|
||||||
WorkflowNodeTypeLetter,
|
WorkflowNodeTypeLetter,
|
||||||
} from '../../../components/Workflow';
|
} from '../../../components/Workflow';
|
||||||
|
import getNodeType from './shared/WorkflowJobTemplateVisualizerUtils';
|
||||||
|
|
||||||
const NodeG = styled.g`
|
const NodeG = styled.g`
|
||||||
pointer-events: ${props => (props.noPointerEvents ? 'none' : 'initial')};
|
pointer-events: ${props => (props.noPointerEvents ? 'none' : 'initial')};
|
||||||
@@ -52,13 +56,85 @@ function VisualizerNode({
|
|||||||
}) {
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
|
const [credentialsError, setCredentialsError] = useState(null);
|
||||||
|
const [detailError, setDetailError] = useState(null);
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const { addingLink, addLinkSourceNode, nodePositions } = useContext(
|
const { addingLink, addLinkSourceNode, nodePositions, nodes } = useContext(
|
||||||
WorkflowStateContext
|
WorkflowStateContext
|
||||||
);
|
);
|
||||||
const isAddLinkSourceNode =
|
const isAddLinkSourceNode =
|
||||||
addLinkSourceNode && addLinkSourceNode.id === node.id;
|
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 = () => {
|
const handleNodeMouseEnter = () => {
|
||||||
ref.current.parentNode.appendChild(ref.current);
|
ref.current.parentNode.appendChild(ref.current);
|
||||||
setHovering(true);
|
setHovering(true);
|
||||||
@@ -91,11 +167,7 @@ function VisualizerNode({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-details"
|
id="node-details"
|
||||||
key="details"
|
key="details"
|
||||||
onClick={() => {
|
onClick={handleViewClick}
|
||||||
updateHelpText(null);
|
|
||||||
setHovering(false);
|
|
||||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: node });
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
@@ -123,11 +195,7 @@ function VisualizerNode({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-edit"
|
id="node-edit"
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => {
|
onClick={handleEditClick}
|
||||||
updateHelpText(null);
|
|
||||||
setHovering(false);
|
|
||||||
dispatch({ type: 'SET_NODE_TO_EDIT', value: node });
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
@@ -164,6 +232,7 @@ function VisualizerNode({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<NodeG
|
<NodeG
|
||||||
id={`node-${node.id}`}
|
id={`node-${node.id}`}
|
||||||
job={node.job}
|
job={node.job}
|
||||||
@@ -171,8 +240,9 @@ function VisualizerNode({
|
|||||||
onMouseEnter={handleNodeMouseEnter}
|
onMouseEnter={handleNodeMouseEnter}
|
||||||
onMouseLeave={handleNodeMouseLeave}
|
onMouseLeave={handleNodeMouseLeave}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id]
|
transform={`translate(${nodePositions[node.id].x},${nodePositions[
|
||||||
.y - nodePositions[1].y})`}
|
node.id
|
||||||
|
].y - nodePositions[1].y})`}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
fill="#FFFFFF"
|
fill="#FFFFFF"
|
||||||
@@ -200,13 +270,14 @@ function VisualizerNode({
|
|||||||
>
|
>
|
||||||
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
||||||
<NodeResourceName id={`node-${node.id}-name`}>
|
<NodeResourceName id={`node-${node.id}-name`}>
|
||||||
{node.unifiedJobTemplate
|
{node?.fullUnifiedJobTemplate?.name ||
|
||||||
? node.unifiedJobTemplate.name
|
node?.originalNodeObject?.summary_fields?.unified_job_template
|
||||||
: i18n._(t`DELETED`)}
|
?.name ||
|
||||||
|
i18n._(t`DELETED`)}
|
||||||
</NodeResourceName>
|
</NodeResourceName>
|
||||||
</NodeContents>
|
</NodeContents>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
{node.unifiedJobTemplate && <WorkflowNodeTypeLetter node={node} />}
|
<WorkflowNodeTypeLetter node={node} />
|
||||||
{hovering && !addingLink && (
|
{hovering && !addingLink && (
|
||||||
<WorkflowActionTooltip
|
<WorkflowActionTooltip
|
||||||
pointX={wfConstants.nodeW}
|
pointX={wfConstants.nodeW}
|
||||||
@@ -215,6 +286,29 @@ function VisualizerNode({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</NodeG>
|
</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>
|
||||||
|
)}
|
||||||
|
{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 React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import {
|
import {
|
||||||
WorkflowDispatchContext,
|
WorkflowDispatchContext,
|
||||||
WorkflowStateContext,
|
WorkflowStateContext,
|
||||||
} from '../../../contexts/Workflow';
|
} from '../../../contexts/Workflow';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { JobTemplatesAPI, WorkflowJobTemplateNodesAPI } from '../../../api';
|
||||||
import VisualizerNode from './VisualizerNode';
|
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 = {
|
const mockedContext = {
|
||||||
addingLink: false,
|
addingLink: false,
|
||||||
@@ -23,15 +44,7 @@ const mockedContext = {
|
|||||||
y: 40,
|
y: 40,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
nodes: [nodeWithJT],
|
||||||
|
|
||||||
const nodeWithJT = {
|
|
||||||
id: 2,
|
|
||||||
unifiedJobTemplate: {
|
|
||||||
id: 77,
|
|
||||||
name: 'Automation JT',
|
|
||||||
type: 'job_template',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
@@ -47,8 +60,6 @@ describe('VisualizerNode', () => {
|
|||||||
<WorkflowStateContext.Provider value={mockedContext}>
|
<WorkflowStateContext.Provider value={mockedContext}>
|
||||||
<svg>
|
<svg>
|
||||||
<VisualizerNode
|
<VisualizerNode
|
||||||
mouseEnter={() => {}}
|
|
||||||
mouseLeave={() => {}}
|
|
||||||
node={nodeWithJT}
|
node={nodeWithJT}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
updateHelpText={updateHelpText}
|
updateHelpText={updateHelpText}
|
||||||
@@ -59,6 +70,9 @@ describe('VisualizerNode', () => {
|
|||||||
</WorkflowDispatchContext.Provider>
|
</WorkflowDispatchContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
@@ -67,10 +81,10 @@ describe('VisualizerNode', () => {
|
|||||||
});
|
});
|
||||||
test('Displays action tooltip on hover and updates help text on hover', () => {
|
test('Displays action tooltip on hover and updates help text on hover', () => {
|
||||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
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('WorkflowActionTooltip').length).toBe(1);
|
||||||
expect(wrapper.find('WorkflowActionTooltipItem').length).toBe(5);
|
expect(wrapper.find('WorkflowActionTooltipItem').length).toBe(5);
|
||||||
wrapper.find('VisualizerNode').simulate('mouseleave');
|
wrapper.find('g').simulate('mouseleave');
|
||||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
wrapper
|
wrapper
|
||||||
.find('foreignObject')
|
.find('foreignObject')
|
||||||
@@ -85,7 +99,7 @@ describe('VisualizerNode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Add tooltip action hover/click updates help text and dispatches properly', () => {
|
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');
|
wrapper.find('WorkflowActionTooltipItem#node-add').simulate('mouseenter');
|
||||||
expect(updateHelpText).toHaveBeenCalledWith('Add a new node');
|
expect(updateHelpText).toHaveBeenCalledWith('Add a new node');
|
||||||
wrapper.find('WorkflowActionTooltipItem#node-add').simulate('mouseleave');
|
wrapper.find('WorkflowActionTooltipItem#node-add').simulate('mouseleave');
|
||||||
@@ -98,8 +112,8 @@ describe('VisualizerNode', () => {
|
|||||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Edit tooltip action hover/click updates help text and dispatches properly', () => {
|
test('Edit tooltip action hover/click updates help text and dispatches properly', async () => {
|
||||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
wrapper.find('g').simulate('mouseenter');
|
||||||
wrapper
|
wrapper
|
||||||
.find('WorkflowActionTooltipItem#node-edit')
|
.find('WorkflowActionTooltipItem#node-edit')
|
||||||
.simulate('mouseenter');
|
.simulate('mouseenter');
|
||||||
@@ -109,15 +123,27 @@ describe('VisualizerNode', () => {
|
|||||||
.simulate('mouseleave');
|
.simulate('mouseleave');
|
||||||
expect(updateHelpText).toHaveBeenCalledWith(null);
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
wrapper.find('WorkflowActionTooltipItem#node-edit').simulate('click');
|
wrapper.find('WorkflowActionTooltipItem#node-edit').simulate('click');
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
await asyncFlush();
|
||||||
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
|
expect(dispatch.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'SET_NODES',
|
||||||
|
value: [nodeWithJT],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
type: 'SET_NODE_TO_EDIT',
|
type: 'SET_NODE_TO_EDIT',
|
||||||
value: nodeWithJT,
|
value: nodeWithJT,
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Details tooltip action hover/click updates help text and dispatches properly', () => {
|
test('Details tooltip action hover/click updates help text and dispatches properly', async () => {
|
||||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
wrapper.find('g').simulate('mouseenter');
|
||||||
wrapper
|
wrapper
|
||||||
.find('WorkflowActionTooltipItem#node-details')
|
.find('WorkflowActionTooltipItem#node-details')
|
||||||
.simulate('mouseenter');
|
.simulate('mouseenter');
|
||||||
@@ -127,15 +153,27 @@ describe('VisualizerNode', () => {
|
|||||||
.simulate('mouseleave');
|
.simulate('mouseleave');
|
||||||
expect(updateHelpText).toHaveBeenCalledWith(null);
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
wrapper.find('WorkflowActionTooltipItem#node-details').simulate('click');
|
wrapper.find('WorkflowActionTooltipItem#node-details').simulate('click');
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
await asyncFlush();
|
||||||
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
|
expect(dispatch.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'SET_NODES',
|
||||||
|
value: [nodeWithJT],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
type: 'SET_NODE_TO_VIEW',
|
type: 'SET_NODE_TO_VIEW',
|
||||||
value: nodeWithJT,
|
value: nodeWithJT,
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Link tooltip action hover/click updates help text and dispatches properly', () => {
|
test('Link tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
wrapper.find('g').simulate('mouseenter');
|
||||||
wrapper
|
wrapper
|
||||||
.find('WorkflowActionTooltipItem#node-link')
|
.find('WorkflowActionTooltipItem#node-link')
|
||||||
.simulate('mouseenter');
|
.simulate('mouseenter');
|
||||||
@@ -153,7 +191,7 @@ describe('VisualizerNode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Delete tooltip action hover/click updates help text and dispatches properly', () => {
|
test('Delete tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
wrapper.find('VisualizerNode').simulate('mouseenter');
|
wrapper.find('g').simulate('mouseenter');
|
||||||
wrapper
|
wrapper
|
||||||
.find('WorkflowActionTooltipItem#node-delete')
|
.find('WorkflowActionTooltipItem#node-delete')
|
||||||
.simulate('mouseenter');
|
.simulate('mouseenter');
|
||||||
@@ -201,12 +239,12 @@ describe('VisualizerNode', () => {
|
|||||||
});
|
});
|
||||||
test('Displays correct help text when hovering over node while adding link', () => {
|
test('Displays correct help text when hovering over node while adding link', () => {
|
||||||
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
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(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
expect(updateHelpText).toHaveBeenCalledWith(
|
expect(updateHelpText).toHaveBeenCalledWith(
|
||||||
'Click to create a new link to this node.'
|
'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(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
expect(updateHelpText).toHaveBeenCalledWith(null);
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
});
|
});
|
||||||
@@ -227,8 +265,6 @@ describe('VisualizerNode', () => {
|
|||||||
<svg>
|
<svg>
|
||||||
<WorkflowStateContext.Provider value={mockedContext}>
|
<WorkflowStateContext.Provider value={mockedContext}>
|
||||||
<VisualizerNode
|
<VisualizerNode
|
||||||
mouseEnter={() => {}}
|
|
||||||
mouseLeave={() => {}}
|
|
||||||
node={{
|
node={{
|
||||||
id: 2,
|
id: 2,
|
||||||
}}
|
}}
|
||||||
@@ -243,4 +279,143 @@ describe('VisualizerNode', () => {
|
|||||||
expect(wrapper.find('NodeResourceName').text()).toBe('DELETED');
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,8 +79,9 @@ function VisualizerToolbar({
|
|||||||
<Badge id="visualizer-total-nodes-badge" isRead>
|
<Badge id="visualizer-total-nodes-badge" isRead>
|
||||||
{totalNodes}
|
{totalNodes}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle legend`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
aria-label={i18n._(t`Toggle legend`)}
|
||||||
id="visualizer-toggle-legend"
|
id="visualizer-toggle-legend"
|
||||||
isActive={totalNodes > 0 && showLegend}
|
isActive={totalNodes > 0 && showLegend}
|
||||||
isDisabled={totalNodes === 0}
|
isDisabled={totalNodes === 0}
|
||||||
@@ -90,8 +91,9 @@ function VisualizerToolbar({
|
|||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle tools`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
aria-label={i18n._(t`Toggle tools`)}
|
||||||
id="visualizer-toggle-tools"
|
id="visualizer-toggle-tools"
|
||||||
isActive={totalNodes > 0 && showTools}
|
isActive={totalNodes > 0 && showTools}
|
||||||
isDisabled={totalNodes === 0}
|
isDisabled={totalNodes === 0}
|
||||||
@@ -101,8 +103,12 @@ function VisualizerToolbar({
|
|||||||
<WrenchIcon />
|
<WrenchIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={i18n._(t`Workflow documentation`)}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
aria-label={i18n._(t`Workflow Documentation`)}
|
aria-label={i18n._(t`Workflow documentation`)}
|
||||||
id="visualizer-documentation"
|
id="visualizer-documentation"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
component="a"
|
component="a"
|
||||||
@@ -111,8 +117,13 @@ function VisualizerToolbar({
|
|||||||
>
|
>
|
||||||
<BookIcon />
|
<BookIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
{template.summary_fields?.user_capabilities?.start && (
|
{template.summary_fields?.user_capabilities?.start && (
|
||||||
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
|
<Tooltip content={i18n._(t`Launch workflow`)} position="bottom">
|
||||||
|
<LaunchButton
|
||||||
|
resource={template}
|
||||||
|
aria-label={i18n._(t`Launch workflow`)}
|
||||||
|
>
|
||||||
{({ handleLaunch }) => (
|
{({ handleLaunch }) => (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
id="visualizer-launch"
|
id="visualizer-launch"
|
||||||
@@ -124,10 +135,11 @@ function VisualizerToolbar({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
</LaunchButton>
|
</LaunchButton>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
|
<Tooltip content={i18n._(t`Delete all nodes`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
id="visualizer-delete-all"
|
id="visualizer-delete-all"
|
||||||
aria-label={i18n._(t`Delete all nodes`)}
|
aria-label={i18n._(t`Delete all nodes`)}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user