Fixes for various prompt related ui issues

Fixes bug where Forks showed up in both default values and prompted values in launch summary

Fixes prompting IGs with defaults on launch

Make job tags and skip tags full width on workflow form

Fixes bug where we attempted to fetch instance groups for workflows

Fetch default instance groups from jt/schedule for schedule form prompt

Grab default IGs when adding a node that prompts for them

Adds support for saving labels on a new wf node

Fix linting errors

Fixes for various prompt on launch related issues

Adds support for saving instance groups on a new node

Adds support for saving instance groups when editing an existing node

Fix workflowReducer test

Updates useSelected to handle a non-empty starting state

Fixes visualizerNode tests

Fix visualizer test

Second batch of prompt related ui issues:

Fixes bug saving existing node when instance groups is not promptable

Fixes bug removing newly added label

Adds onError function to label prompt

Fixes tooltips on the other prompts step

Properly fetch all labels to show on schedule details
This commit is contained in:
mabashian
2022-09-06 14:34:40 -04:00
committed by Alan Rominger
parent e076f1ee2a
commit e05eaeccab
24 changed files with 402 additions and 109 deletions

View File

@@ -1,6 +1,8 @@
import Base from '../Base'; import Base from '../Base';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
import LabelsMixin from '../mixins/Labels.mixin';
class WorkflowJobTemplateNodes extends Base { class WorkflowJobTemplateNodes extends LabelsMixin(InstanceGroupsMixin(Base)) {
constructor(http) { constructor(http) {
super(http); super(http);
this.baseUrl = 'api/v2/workflow_job_template_nodes/'; this.baseUrl = 'api/v2/workflow_job_template_nodes/';

View File

@@ -91,11 +91,8 @@ function LabelSelect({ value, placeholder, onChange, onError, createText }) {
<Chip <Chip
isReadOnly={currentChip.isReadOnly} isReadOnly={currentChip.isReadOnly}
key={currentChip.name} key={currentChip.name}
onClick={(e, item) => { onClick={(e) => {
if (typeof item === 'string') { onSelect(e, currentChip);
item = { id: item, name: item };
}
onSelect(e, item);
}} }}
> >
{currentChip.name} {currentChip.name}

View File

@@ -42,6 +42,7 @@ function LaunchButton({ resource, children }) {
const [launchConfig, setLaunchConfig] = useState(null); const [launchConfig, setLaunchConfig] = useState(null);
const [surveyConfig, setSurveyConfig] = useState(null); const [surveyConfig, setSurveyConfig] = useState(null);
const [labels, setLabels] = useState([]); const [labels, setLabels] = useState([]);
const [instanceGroups, setInstanceGroups] = useState([]);
const [isLaunching, setIsLaunching] = useState(false); const [isLaunching, setIsLaunching] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -83,6 +84,14 @@ function LaunchButton({ resource, children }) {
setLabels(allLabels); setLabels(allLabels);
} }
if (launch.ask_instance_groups_on_launch) {
const {
data: { results },
} = await JobTemplatesAPI.readInstanceGroups(resource.id);
setInstanceGroups(results);
}
if (canLaunchWithoutPrompt(launch)) { if (canLaunchWithoutPrompt(launch)) {
await launchWithParams({}); await launchWithParams({});
} else { } else {
@@ -197,6 +206,7 @@ function LaunchButton({ resource, children }) {
labels={labels} labels={labels}
onLaunch={launchWithParams} onLaunch={launchWithParams}
onCancel={() => setShowLaunchPrompt(false)} onCancel={() => setShowLaunchPrompt(false)}
instanceGroups={instanceGroups}
/> />
)} )}
</> </>

View File

@@ -18,6 +18,7 @@ function PromptModalForm({
resource, resource,
labels, labels,
surveyConfig, surveyConfig,
instanceGroups,
}) { }) {
const { setFieldTouched, values } = useFormikContext(); const { setFieldTouched, values } = useFormikContext();
const [showDescription, setShowDescription] = useState(false); const [showDescription, setShowDescription] = useState(false);
@@ -29,7 +30,13 @@ function PromptModalForm({
visitStep, visitStep,
visitAllSteps, visitAllSteps,
contentError, contentError,
} = useLaunchSteps(launchConfig, surveyConfig, resource, labels); } = useLaunchSteps(
launchConfig,
surveyConfig,
resource,
labels,
instanceGroups
);
const handleSubmit = async () => { const handleSubmit = async () => {
const postValues = {}; const postValues = {};
@@ -197,6 +204,7 @@ function LaunchPrompt({
labels = [], labels = [],
surveyConfig, surveyConfig,
resourceDefaultCredentials = [], resourceDefaultCredentials = [],
instanceGroups = [],
}) { }) {
return ( return (
<Formik initialValues={{}} onSubmit={(values) => onLaunch(values)}> <Formik initialValues={{}} onSubmit={(values) => onLaunch(values)}>
@@ -208,6 +216,7 @@ function LaunchPrompt({
resource={resource} resource={resource}
labels={labels} labels={labels}
resourceDefaultCredentials={resourceDefaultCredentials} resourceDefaultCredentials={resourceDefaultCredentials}
instanceGroups={instanceGroups}
/> />
</Formik> </Formik>
); );

View File

@@ -19,7 +19,7 @@ const QS_CONFIG = getQSConfig('instance-groups', {
function InstanceGroupsStep() { function InstanceGroupsStep() {
const [field, , helpers] = useField('instance_groups'); const [field, , helpers] = useField('instance_groups');
const { selected, handleSelect, setSelected } = useSelected([]); const { selected, handleSelect, setSelected } = useSelected([], field.value);
const history = useHistory(); const history = useHistory();
@@ -69,7 +69,7 @@ function InstanceGroupsStep() {
return ( return (
<OptionsList <OptionsList
value={field.value} value={selected}
options={instance_groups} options={instance_groups}
optionCount={count} optionCount={count}
searchColumns={[ searchColumns={[

View File

@@ -10,6 +10,8 @@ import AnsibleSelect from '../../AnsibleSelect';
import { VariablesField } from '../../CodeEditor'; import { VariablesField } from '../../CodeEditor';
import Popover from '../../Popover'; import Popover from '../../Popover';
import { VerbositySelectField } from '../../VerbositySelectField'; import { VerbositySelectField } from '../../VerbositySelectField';
import jobHelpText from '../../../screens/Job/Job.helptext';
import workflowHelpText from '../../../screens/Template/shared/WorkflowJobTemplate.helptext';
const FieldHeader = styled.div` const FieldHeader = styled.div`
display: flex; display: flex;
@@ -22,22 +24,29 @@ const FieldHeader = styled.div`
`; `;
function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) { function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
const helpTextSource = launchConfig.job_template_data
? jobHelpText
: workflowHelpText;
return ( return (
<Form <Form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
}} }}
> >
{launchConfig.ask_job_type_on_launch && <JobTypeField />} {launchConfig.ask_job_type_on_launch && (
<JobTypeField helpTextSource={helpTextSource} />
)}
{launchConfig.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"
label={t`Source Control Branch`} label={t`Source Control Branch`}
tooltip={t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch`} tooltip={helpTextSource.sourceControlBranch}
/> />
)} )}
{launchConfig.ask_labels_on_launch && <LabelsField />} {launchConfig.ask_labels_on_launch && (
<LabelsField helpTextSource={helpTextSource} />
)}
{launchConfig.ask_forks_on_launch && ( {launchConfig.ask_forks_on_launch && (
<FormField <FormField
id="prompt-forks" id="prompt-forks"
@@ -45,6 +54,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
label={t`Forks`} label={t`Forks`}
type="number" type="number"
min="0" min="0"
tooltip={helpTextSource.forks}
/> />
)} )}
{launchConfig.ask_limit_on_launch && ( {launchConfig.ask_limit_on_launch && (
@@ -52,13 +62,12 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
id="prompt-limit" id="prompt-limit"
name="limit" name="limit"
label={t`Limit`} label={t`Limit`}
tooltip={t`Provide a host pattern to further constrain the list tooltip={helpTextSource.limit}
of hosts that will be managed or affected by the playbook. Multiple
patterns are allowed. Refer to Ansible documentation for more
information and examples on patterns.`}
/> />
)} )}
{launchConfig.ask_verbosity_on_launch && <VerbosityField />} {launchConfig.ask_verbosity_on_launch && (
<VerbosityField helpTextSource={helpTextSource} />
)}
{launchConfig.ask_job_slice_count_on_launch && ( {launchConfig.ask_job_slice_count_on_launch && (
<FormField <FormField
id="prompt-job-slicing" id="prompt-job-slicing"
@@ -66,6 +75,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
label={t`Job Slicing`} label={t`Job Slicing`}
type="number" type="number"
min="1" min="1"
tooltip={helpTextSource.jobSlicing}
/> />
)} )}
{launchConfig.ask_timeout_on_launch && ( {launchConfig.ask_timeout_on_launch && (
@@ -75,6 +85,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
label={t`Timeout`} label={t`Timeout`}
type="number" type="number"
min="0" min="0"
tooltip={helpTextSource.timeout}
/> />
)} )}
{launchConfig.ask_diff_mode_on_launch && <ShowChangesToggle />} {launchConfig.ask_diff_mode_on_launch && <ShowChangesToggle />}
@@ -84,10 +95,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
name="job_tags" name="job_tags"
label={t`Job Tags`} label={t`Job Tags`}
aria-label={t`Job Tags`} aria-label={t`Job Tags`}
tooltip={t`Tags are useful when you have a large tooltip={helpTextSource.jobTags}
playbook, and you want to run a specific part of a play or task.
Use commas to separate multiple tags. Refer to Ansible Controller
documentation for details on the usage of tags.`}
/> />
)} )}
{launchConfig.ask_skip_tags_on_launch && ( {launchConfig.ask_skip_tags_on_launch && (
@@ -96,10 +104,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
name="skip_tags" name="skip_tags"
label={t`Skip Tags`} label={t`Skip Tags`}
aria-label={t`Skip Tags`} aria-label={t`Skip Tags`}
tooltip={t`Skip tags are useful when you have a large tooltip={helpTextSource.skipTags}
playbook, and you want to skip specific parts of a play or task.
Use commas to separate multiple tags. Refer to Ansible Controller
documentation for details on the usage of tags.`}
/> />
)} )}
{launchConfig.ask_variables_on_launch && ( {launchConfig.ask_variables_on_launch && (
@@ -115,7 +120,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
); );
} }
function JobTypeField() { function JobTypeField({ helpTextSource }) {
const [field, meta, helpers] = useField('job_type'); const [field, meta, helpers] = useField('job_type');
const options = [ const options = [
{ {
@@ -135,15 +140,9 @@ function JobTypeField() {
const isValid = !(meta.touched && meta.error); const isValid = !(meta.touched && meta.error);
return ( return (
<FormGroup <FormGroup
fieldId="propmt-job-type" fieldId="prompt-job-type"
label={t`Job Type`} label={t`Job Type`}
labelIcon={ labelIcon={<Popover content={helpTextSource.jobType} />}
<Popover
content={t`For job templates, select run to execute the playbook.
Select check to only check playbook syntax, test environment setup,
and report problems without executing the playbook.`}
/>
}
isRequired isRequired
validated={isValid ? 'default' : 'error'} validated={isValid ? 'default' : 'error'}
> >
@@ -157,15 +156,14 @@ function JobTypeField() {
); );
} }
function VerbosityField() { function VerbosityField({ helpTextSource }) {
const [, meta] = useField('verbosity'); const [, meta] = useField('verbosity');
const isValid = !(meta.touched && meta.error); const isValid = !(meta.touched && meta.error);
return ( return (
<VerbositySelectField <VerbositySelectField
fieldId="prompt-verbosity" fieldId="prompt-verbosity"
tooltip={t`Control the level of output ansible tooltip={helpTextSource.verbosity}
will produce as the playbook executes.`}
isValid={isValid ? 'default' : 'error'} isValid={isValid ? 'default' : 'error'}
/> />
); );
@@ -214,24 +212,22 @@ function TagField({ id, name, label, tooltip }) {
); );
} }
function LabelsField() { function LabelsField({ helpTextSource }) {
const [field, , helpers] = useField('labels'); const [field, meta, helpers] = useField('labels');
return ( return (
<FormGroup <FormGroup
fieldId="propmt-labels" fieldId="prompt-labels"
label={t`Labels`} label={t`Labels`}
labelIcon={ labelIcon={<Popover content={helpTextSource.labels} />}
<Popover validated={!meta.touched || !meta.error ? 'default' : 'error'}
content={t`Optional labels that describe this job, such as 'dev' or 'test'. Labels can be used to group and filter completed jobs.`} helperTextInvalid={meta.error}
/>
}
> >
<LabelSelect <LabelSelect
value={field.value} value={field.value}
onChange={(labels) => helpers.setValue(labels)} onChange={(labels) => helpers.setValue(labels)}
createText={t`Create`} createText={t`Create`}
onError={() => {}} onError={(err) => helpers.setError(err)}
/> />
</FormGroup> </FormGroup>
); );

View File

@@ -13,6 +13,11 @@ describe('OtherPromptsStep', () => {
<OtherPromptsStep <OtherPromptsStep
launchConfig={{ launchConfig={{
ask_job_type_on_launch: true, ask_job_type_on_launch: true,
job_template_data: {
name: 'Demo Job Template',
id: 1,
description: '',
},
}} }}
/> />
</Formik> </Formik>
@@ -36,6 +41,11 @@ describe('OtherPromptsStep', () => {
<OtherPromptsStep <OtherPromptsStep
launchConfig={{ launchConfig={{
ask_limit_on_launch: true, ask_limit_on_launch: true,
job_template_data: {
name: 'Demo Job Template',
id: 1,
description: '',
},
}} }}
/> />
</Formik> </Formik>
@@ -56,6 +66,11 @@ describe('OtherPromptsStep', () => {
<OtherPromptsStep <OtherPromptsStep
launchConfig={{ launchConfig={{
ask_scm_branch_on_launch: true, ask_scm_branch_on_launch: true,
job_template_data: {
name: 'Demo Job Template',
id: 1,
description: '',
},
}} }}
/> />
</Formik> </Formik>
@@ -76,6 +91,11 @@ describe('OtherPromptsStep', () => {
<OtherPromptsStep <OtherPromptsStep
launchConfig={{ launchConfig={{
ask_verbosity_on_launch: true, ask_verbosity_on_launch: true,
job_template_data: {
name: 'Demo Job Template',
id: 1,
description: '',
},
}} }}
/> />
</Formik> </Formik>
@@ -96,6 +116,11 @@ describe('OtherPromptsStep', () => {
<OtherPromptsStep <OtherPromptsStep
launchConfig={{ launchConfig={{
ask_diff_mode_on_launch: true, ask_diff_mode_on_launch: true,
job_template_data: {
name: 'Demo Job Template',
id: 1,
description: '',
},
}} }}
/> />
</Formik> </Formik>
@@ -119,6 +144,11 @@ describe('OtherPromptsStep', () => {
onVarModeChange={onModeChange} onVarModeChange={onModeChange}
launchConfig={{ launchConfig={{
ask_variables_on_launch: true, ask_variables_on_launch: true,
job_template_data: {
name: 'Demo Job Template',
id: 1,
description: '',
},
}} }}
/> />
</Formik> </Formik>

View File

@@ -45,7 +45,8 @@ export default function useLaunchSteps(
launchConfig, launchConfig,
surveyConfig, surveyConfig,
resource, resource,
labels labels,
instanceGroups
) { ) {
const [visited, setVisited] = useState({}); const [visited, setVisited] = useState({});
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
@@ -64,7 +65,7 @@ export default function useLaunchSteps(
visited visited
), ),
useExecutionEnvironmentStep(launchConfig, resource), useExecutionEnvironmentStep(launchConfig, resource),
useInstanceGroupsStep(launchConfig, resource), useInstanceGroupsStep(launchConfig, resource, instanceGroups),
useOtherPromptsStep(launchConfig, resource, labels), useOtherPromptsStep(launchConfig, resource, labels),
useSurveyStep(launchConfig, surveyConfig, resource, visited), useSurveyStep(launchConfig, surveyConfig, resource, visited),
]; ];

View File

@@ -17,7 +17,7 @@ export default function useSyncedSelectValue(value, onChange) {
return; return;
} }
const newOptions = []; const newOptions = [];
if (value !== selections && options.length) { if (value && value !== selections && options.length) {
const syncedValue = value.map((item) => { const syncedValue = value.map((item) => {
const match = options.find((i) => i.id === item.id); const match = options.find((i) => i.id === item.id);
if (!match) { if (!match) {

View File

@@ -146,7 +146,10 @@ function PromptJobTemplateDetail({ resource }) {
/> />
<Detail label={t`Source Control Branch`} value={scm_branch} /> <Detail label={t`Source Control Branch`} value={scm_branch} />
<Detail label={t`Playbook`} value={playbook} /> <Detail label={t`Playbook`} value={playbook} />
<Detail label={t`Forks`} value={forks || '0'} /> <Detail
label={t`Forks`}
value={typeof forks === 'number' ? forks.toString() : forks}
/>
<Detail label={t`Limit`} value={limit} /> <Detail label={t`Limit`} value={limit} />
<Detail label={t`Verbosity`} value={VERBOSITY()[verbosity]} /> <Detail label={t`Verbosity`} value={VERBOSITY()[verbosity]} />
{typeof diff_mode === 'boolean' && ( {typeof diff_mode === 'boolean' && (

View File

@@ -27,6 +27,11 @@ import { VariablesDetail } from '../../CodeEditor';
import { VERBOSITY } from '../../VerbositySelectField'; import { VERBOSITY } from '../../VerbositySelectField';
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext'; import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
const buildLinkURL = (instance) =>
instance.is_container_group
? '/instance_groups/container_group/'
: '/instance_groups/';
const PromptDivider = styled(Divider)` const PromptDivider = styled(Divider)`
margin-top: var(--pf-global--spacer--lg); margin-top: var(--pf-global--spacer--lg);
margin-bottom: var(--pf-global--spacer--lg); margin-bottom: var(--pf-global--spacer--lg);
@@ -80,7 +85,6 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
job_slice_count, job_slice_count,
job_tags, job_tags,
job_type, job_type,
labels,
limit, limit,
modified, modified,
name, name,
@@ -113,7 +117,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
const { error, dismissError } = useDismissableError(deleteError); const { error, dismissError } = useDismissableError(deleteError);
const { const {
result: [credentials, preview, launchData], result: [credentials, preview, launchData, labels, instanceGroups],
isLoading, isLoading,
error: readContentError, error: readContentError,
request: fetchCredentialsAndPreview, request: fetchCredentialsAndPreview,
@@ -133,7 +137,9 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
promises.push( promises.push(
JobTemplatesAPI.readLaunch( JobTemplatesAPI.readLaunch(
schedule.summary_fields.unified_job_template.id schedule.summary_fields.unified_job_template.id
) ),
SchedulesAPI.readAllLabels(id),
SchedulesAPI.readInstanceGroups(id)
); );
} else if ( } else if (
schedule?.summary_fields?.unified_job_template?.unified_job_type === schedule?.summary_fields?.unified_job_template?.unified_job_type ===
@@ -142,17 +148,28 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
promises.push( promises.push(
WorkflowJobTemplatesAPI.readLaunch( WorkflowJobTemplatesAPI.readLaunch(
schedule.summary_fields.unified_job_template.id schedule.summary_fields.unified_job_template.id
) ),
SchedulesAPI.readAllLabels(id)
); );
} else { } else {
promises.push(Promise.resolve()); promises.push(Promise.resolve());
} }
const [{ data }, { data: schedulePreview }, launch] = await Promise.all( const [
promises { data },
); { data: schedulePreview },
launch,
allLabelsResults,
instanceGroupsResults,
] = await Promise.all(promises);
return [data.results, schedulePreview, launch?.data]; return [
data.results,
schedulePreview,
launch?.data,
allLabelsResults?.data?.results,
instanceGroupsResults?.data?.results,
];
}, [id, schedule, rrule]), }, [id, schedule, rrule]),
[] []
); );
@@ -195,6 +212,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
ask_forks_on_launch, ask_forks_on_launch,
ask_job_slice_count_on_launch, ask_job_slice_count_on_launch,
ask_timeout_on_launch, ask_timeout_on_launch,
ask_instance_groups_on_launch,
survey_enabled, survey_enabled,
} = launchData || {}; } = launchData || {};
@@ -255,6 +273,8 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
const showForksDetail = ask_forks_on_launch; const showForksDetail = ask_forks_on_launch;
const showJobSlicingDetail = ask_job_slice_count_on_launch; const showJobSlicingDetail = ask_job_slice_count_on_launch;
const showTimeoutDetail = ask_timeout_on_launch; const showTimeoutDetail = ask_timeout_on_launch;
const showInstanceGroupsDetail =
ask_instance_groups_on_launch && instanceGroups.length > 0;
const showPromptedFields = const showPromptedFields =
showCredentialsDetail || showCredentialsDetail ||
@@ -271,7 +291,8 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
showLabelsDetail || showLabelsDetail ||
showForksDetail || showForksDetail ||
showJobSlicingDetail || showJobSlicingDetail ||
showTimeoutDetail; showTimeoutDetail ||
showInstanceGroupsDetail;
if (isLoading) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
@@ -471,6 +492,35 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
{ask_job_slice_count_on_launch && ( {ask_job_slice_count_on_launch && (
<Detail label={t`Job Slicing`} value={job_slice_count} /> <Detail label={t`Job Slicing`} value={job_slice_count} />
)} )}
{showInstanceGroupsDetail && (
<Detail
fullWidth
label={t`Instance Groups`}
value={
<ChipGroup
numChips={5}
totalChips={instanceGroups.length}
ouiaId="instance-group-chips"
>
{instanceGroups.map((ig) => (
<Link
to={`${buildLinkURL(ig)}${ig.id}/details`}
key={ig.id}
>
<Chip
key={ig.id}
ouiaId={`instance-group-${ig.id}-chip`}
isReadOnly
>
{ig.name}
</Chip>
</Link>
))}
</ChipGroup>
}
isEmpty={instanceGroups.length === 0}
/>
)}
{showCredentialsDetail && ( {showCredentialsDetail && (
<Detail <Detail
fullWidth fullWidth

View File

@@ -54,13 +54,14 @@ function ScheduleForm({
request: loadScheduleData, request: loadScheduleData,
error: contentError, error: contentError,
isLoading: contentLoading, isLoading: contentLoading,
result: { zoneOptions, zoneLinks, credentials, labels }, result: { zoneOptions, zoneLinks, credentials, labels, instanceGroups },
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const { data } = await SchedulesAPI.readZoneInfo(); const { data } = await SchedulesAPI.readZoneInfo();
let creds = []; let creds = [];
let allLabels; let allLabels = [];
let allInstanceGroups = [];
if (schedule.id) { if (schedule.id) {
if ( if (
resource.type === 'job_template' && resource.type === 'job_template' &&
@@ -77,15 +78,30 @@ function ScheduleForm({
} = await SchedulesAPI.readAllLabels(schedule.id); } = await SchedulesAPI.readAllLabels(schedule.id);
allLabels = results; allLabels = results;
} }
} else {
if ( if (
resource.type === 'job_template' && resource.type === 'job_template' &&
launchConfig.ask_labels_on_launch launchConfig.ask_instance_groups_on_launch
) { ) {
const { const {
data: { results }, data: { results },
} = await JobTemplatesAPI.readAllLabels(resource.id); } = await SchedulesAPI.readInstanceGroups(schedule.id);
allLabels = results; allInstanceGroups = results;
}
} else {
if (resource.type === 'job_template') {
if (launchConfig.ask_labels_on_launch) {
const {
data: { results },
} = await JobTemplatesAPI.readAllLabels(resource.id);
allLabels = results;
}
if (launchConfig.ask_instance_groups_on_launch) {
const {
data: { results },
} = await JobTemplatesAPI.readInstanceGroups(resource.id);
allInstanceGroups = results;
}
} }
if ( if (
resource.type === 'workflow_job_template' && resource.type === 'workflow_job_template' &&
@@ -108,13 +124,15 @@ function ScheduleForm({
zoneOptions: zones, zoneOptions: zones,
zoneLinks: data.links, zoneLinks: data.links,
credentials: creds, credentials: creds,
labels: allLabels || [], labels: allLabels,
instanceGroups: allInstanceGroups,
}; };
}, [ }, [
schedule, schedule,
resource.id, resource.id,
resource.type, resource.type,
launchConfig.ask_labels_on_launch, launchConfig.ask_labels_on_launch,
launchConfig.ask_instance_groups_on_launch,
launchConfig.ask_credential_on_launch, launchConfig.ask_credential_on_launch,
]), ]),
{ {
@@ -123,6 +141,7 @@ function ScheduleForm({
credentials: [], credentials: [],
isLoading: true, isLoading: true,
labels: [], labels: [],
instanceGroups: [],
} }
); );
@@ -501,6 +520,7 @@ function ScheduleForm({
}} }}
resourceDefaultCredentials={resourceDefaultCredentials} resourceDefaultCredentials={resourceDefaultCredentials}
labels={labels} labels={labels}
instanceGroups={instanceGroups}
/> />
)} )}
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />

View File

@@ -18,6 +18,7 @@ function SchedulePromptableFields({
resource, resource,
resourceDefaultCredentials, resourceDefaultCredentials,
labels, labels,
instanceGroups,
}) { }) {
const { setFieldTouched, values, initialValues, resetForm } = const { setFieldTouched, values, initialValues, resetForm } =
useFormikContext(); useFormikContext();
@@ -35,7 +36,8 @@ function SchedulePromptableFields({
resource, resource,
credentials, credentials,
resourceDefaultCredentials, resourceDefaultCredentials,
labels labels,
instanceGroups
); );
const [showDescription, setShowDescription] = useState(false); const [showDescription, setShowDescription] = useState(false);
const { error, dismissError } = useDismissableError(contentError); const { error, dismissError } = useDismissableError(contentError);

View File

@@ -16,7 +16,8 @@ export default function useSchedulePromptSteps(
resource, resource,
scheduleCredentials, scheduleCredentials,
resourceDefaultCredentials, resourceDefaultCredentials,
labels labels,
instanceGroups
) { ) {
const sourceOfValues = const sourceOfValues =
(Object.keys(schedule).length > 0 && schedule) || resource; (Object.keys(schedule).length > 0 && schedule) || resource;
@@ -31,7 +32,7 @@ export default function useSchedulePromptSteps(
resourceDefaultCredentials resourceDefaultCredentials
), ),
useExecutionEnvironmentStep(launchConfig, resource), useExecutionEnvironmentStep(launchConfig, resource),
useInstanceGroupsStep(launchConfig, resource), useInstanceGroupsStep(launchConfig, resource, instanceGroups),
useOtherPromptsStep(launchConfig, sourceOfValues, labels), useOtherPromptsStep(launchConfig, sourceOfValues, labels),
useSurveyStep(launchConfig, surveyConfig, sourceOfValues, visited), useSurveyStep(launchConfig, surveyConfig, sourceOfValues, visited),
]; ];

View File

@@ -8,6 +8,7 @@ export function initReducer() {
addNodeTarget: null, addNodeTarget: null,
addingLink: false, addingLink: false,
contentError: null, contentError: null,
defaultOrganization: null,
isLoading: true, isLoading: true,
linkToDelete: null, linkToDelete: null,
linkToEdit: null, linkToEdit: null,
@@ -64,6 +65,11 @@ export default function visualizerReducer(state, action) {
...state, ...state,
contentError: action.value, contentError: action.value,
}; };
case 'SET_DEFAULT_ORGANIZATION':
return {
...state,
defaultOrganization: action.value,
};
case 'SET_IS_LOADING': case 'SET_IS_LOADING':
return { return {
...state, ...state,

View File

@@ -7,6 +7,7 @@ const defaultState = {
addNodeTarget: null, addNodeTarget: null,
addingLink: false, addingLink: false,
contentError: null, contentError: null,
defaultOrganization: null,
isLoading: true, isLoading: true,
linkToDelete: null, linkToDelete: null,
linkToEdit: null, linkToEdit: null,
@@ -1281,6 +1282,18 @@ describe('Workflow reducer', () => {
}); });
}); });
}); });
describe('SET_DEFAULT_ORGANIZATION', () => {
it('should set the state variable', () => {
const result = workflowReducer(defaultState, {
type: 'SET_DEFAULT_ORGANIZATION',
value: 1,
});
expect(result).toEqual({
...defaultState,
defaultOrganization: 1,
});
});
});
describe('SET_IS_LOADING', () => { describe('SET_IS_LOADING', () => {
it('should set the state variable', () => { it('should set the state variable', () => {
const result = workflowReducer(defaultState, { const result = workflowReducer(defaultState, {

View File

@@ -12,8 +12,8 @@ import { useState, useCallback } from 'react';
* } * }
*/ */
export default function useSelected(list = []) { export default function useSelected(list = [], defaultSelected = []) {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState(defaultSelected);
const isAllSelected = selected.length > 0 && selected.length === list.length; const isAllSelected = selected.length > 0 && selected.length === list.length;
const handleSelect = (row) => { const handleSelect = (row) => {

View File

@@ -39,6 +39,7 @@ function NodeModalForm({
isLaunchLoading, isLaunchLoading,
resourceDefaultCredentials, resourceDefaultCredentials,
labels, labels,
instanceGroups,
}) { }) {
const history = useHistory(); const history = useHistory();
const dispatch = useContext(WorkflowDispatchContext); const dispatch = useContext(WorkflowDispatchContext);
@@ -68,7 +69,8 @@ function NodeModalForm({
values.nodeResource, values.nodeResource,
askLinkType, askLinkType,
resourceDefaultCredentials, resourceDefaultCredentials,
labels labels,
instanceGroups
); );
const handleSaveNode = () => { const handleSaveNode = () => {
@@ -243,7 +245,13 @@ const NodeModalInner = ({ title, ...rest }) => {
const { const {
request: readLaunchConfigs, request: readLaunchConfigs,
error: launchConfigError, error: launchConfigError,
result: { launchConfig, surveyConfig, resourceDefaultCredentials, labels }, result: {
launchConfig,
surveyConfig,
resourceDefaultCredentials,
labels,
instanceGroups,
},
isLoading, isLoading,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
@@ -263,6 +271,7 @@ const NodeModalInner = ({ title, ...rest }) => {
surveyConfig: {}, surveyConfig: {},
resourceDefaultCredentials: [], resourceDefaultCredentials: [],
labels: [], labels: [],
instanceGroups: [],
}; };
} }
@@ -309,11 +318,27 @@ const NodeModalInner = ({ title, ...rest }) => {
defaultLabels = results; defaultLabels = results;
} }
let defaultInstanceGroups = [];
if (launch.ask_instance_groups_on_launch) {
const {
data: { results },
} = await await JobTemplatesAPI.readInstanceGroups(
values?.nodeResource?.id,
{
page_size: 200,
}
);
defaultInstanceGroups = results;
}
return { return {
launchConfig: launch, launchConfig: launch,
surveyConfig: survey, surveyConfig: survey,
resourceDefaultCredentials: defaultCredentials, resourceDefaultCredentials: defaultCredentials,
labels: defaultLabels, labels: defaultLabels,
instanceGroups: defaultInstanceGroups,
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -367,11 +392,12 @@ const NodeModalInner = ({ title, ...rest }) => {
isLaunchLoading={isLoading} isLaunchLoading={isLoading}
title={wizardTitle} title={wizardTitle}
labels={labels} labels={labels}
instanceGroups={instanceGroups}
/> />
); );
}; };
const NodeModal = ({ onSave, askLinkType, title, labels }) => { const NodeModal = ({ onSave, askLinkType, title }) => {
const { nodeToEdit } = useContext(WorkflowStateContext); const { nodeToEdit } = useContext(WorkflowStateContext);
const onSaveForm = (values, config) => { const onSaveForm = (values, config) => {
onSave(values, config); onSave(values, config);
@@ -398,7 +424,6 @@ const NodeModal = ({ onSave, askLinkType, title, labels }) => {
onSave={onSaveForm} onSave={onSaveForm}
title={title} title={title}
askLinkType={askLinkType} askLinkType={askLinkType}
labels={labels}
/> />
</Form> </Form>
)} )}

View File

@@ -228,6 +228,12 @@ const getNodeToEditDefaultValues = (
if (launchConfig.ask_timeout_on_launch) { if (launchConfig.ask_timeout_on_launch) {
initialValues.timeout = sourceOfValues?.timeout || 0; initialValues.timeout = sourceOfValues?.timeout || 0;
} }
if (launchConfig.ask_labels_on_launch) {
initialValues.labels = sourceOfValues?.labels || [];
}
if (launchConfig.ask_instance_groups_on_launch) {
initialValues.instance_groups = sourceOfValues?.instance_groups || [];
}
if (launchConfig.ask_variables_on_launch) { if (launchConfig.ask_variables_on_launch) {
const newExtraData = { ...sourceOfValues.extra_data }; const newExtraData = { ...sourceOfValues.extra_data };
@@ -274,7 +280,8 @@ export default function useWorkflowNodeSteps(
resource, resource,
askLinkType, askLinkType,
resourceDefaultCredentials, resourceDefaultCredentials,
labels labels,
instanceGroups
) { ) {
const { nodeToEdit } = useContext(WorkflowStateContext); const { nodeToEdit } = useContext(WorkflowStateContext);
const { const {
@@ -291,7 +298,7 @@ export default function useWorkflowNodeSteps(
useInventoryStep(launchConfig, resource, visited), useInventoryStep(launchConfig, resource, visited),
useCredentialsStep(launchConfig, resource, resourceDefaultCredentials), useCredentialsStep(launchConfig, resource, resourceDefaultCredentials),
useExecutionEnvironmentStep(launchConfig, resource), useExecutionEnvironmentStep(launchConfig, resource),
useInstanceGroupsStep(launchConfig, resource), useInstanceGroupsStep(launchConfig, resource, instanceGroups),
useOtherPromptsStep(launchConfig, resource, labels), useOtherPromptsStep(launchConfig, resource, labels),
useSurveyStep(launchConfig, surveyConfig, resource, visited), useSurveyStep(launchConfig, surveyConfig, resource, visited),
]; ];

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useReducer } from 'react'; import React, { useCallback, useEffect, useReducer } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
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 { t } from '@lingui/macro';
@@ -18,6 +17,7 @@ import ContentLoading from 'components/ContentLoading';
import workflowReducer from 'components/Workflow/workflowReducer'; import workflowReducer from 'components/Workflow/workflowReducer';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import { import {
OrganizationsAPI,
WorkflowApprovalTemplatesAPI, WorkflowApprovalTemplatesAPI,
WorkflowJobTemplateNodesAPI, WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI, WorkflowJobTemplatesAPI,
@@ -53,7 +53,18 @@ const Wrapper = styled.div`
`; `;
const replaceIdentifier = (node) => { const replaceIdentifier = (node) => {
if (stringIsUUID(node.originalNodeObject.identifier) || node.identifier) { if (
stringIsUUID(node.originalNodeObject.identifier) &&
typeof node.identifier === 'string' &&
node.identifier !== ''
) {
return true;
}
if (
!stringIsUUID(node.originalNodeObject.identifier) &&
node.originalNodeObject.identifier !== node.identifier
) {
return true; return true;
} }
@@ -126,6 +137,7 @@ function Visualizer({ template }) {
addNodeTarget: null, addNodeTarget: null,
addingLink: false, addingLink: false,
contentError: null, contentError: null,
defaultOrganization: null,
isLoading: true, isLoading: true,
linkToDelete: null, linkToDelete: null,
linkToEdit: null, linkToEdit: null,
@@ -148,6 +160,7 @@ function Visualizer({ template }) {
addLinkTargetNode, addLinkTargetNode,
addNodeSource, addNodeSource,
contentError, contentError,
defaultOrganization,
isLoading, isLoading,
linkToDelete, linkToDelete,
linkToEdit, linkToEdit,
@@ -261,6 +274,14 @@ function Visualizer({ template }) {
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
try { try {
const {
data: { results },
} = await OrganizationsAPI.read({ page_size: 1, page: 1 });
dispatch({
type: 'SET_DEFAULT_ORGANIZATION',
value: results[0]?.id,
});
const workflowNodes = await fetchWorkflowNodes(template.id); const workflowNodes = await fetchWorkflowNodes(template.id);
dispatch({ dispatch({
type: 'GENERATE_NODES_AND_LINKS', type: 'GENERATE_NODES_AND_LINKS',
@@ -302,6 +323,9 @@ function Visualizer({ template }) {
const deletedNodeIds = []; const deletedNodeIds = [];
const associateCredentialRequests = []; const associateCredentialRequests = [];
const disassociateCredentialRequests = []; const disassociateCredentialRequests = [];
const associateLabelRequests = [];
const disassociateLabelRequests = [];
const instanceGroupRequests = [];
const generateLinkMapAndNewLinks = () => { const generateLinkMapAndNewLinks = () => {
const linkMap = {}; const linkMap = {};
@@ -400,6 +424,8 @@ function Visualizer({ template }) {
nodeRequests.push( nodeRequests.push(
WorkflowJobTemplatesAPI.createNode(template.id, { WorkflowJobTemplatesAPI.createNode(template.id, {
...node.promptValues, ...node.promptValues,
execution_environment:
node.promptValues?.execution_environment?.id || null,
inventory: node.promptValues?.inventory?.id || null, inventory: node.promptValues?.inventory?.id || null,
unified_job_template: node.fullUnifiedJobTemplate.id, unified_job_template: node.fullUnifiedJobTemplate.id,
all_parents_must_converge: node.all_parents_must_converge, all_parents_must_converge: node.all_parents_must_converge,
@@ -423,6 +449,29 @@ function Visualizer({ template }) {
); );
}); });
} }
if (node.promptValues?.labels?.length > 0) {
node.promptValues.labels.forEach((label) => {
associateLabelRequests.push(
WorkflowJobTemplateNodesAPI.associateLabel(
data.id,
label,
node.fullUnifiedJobTemplate.organization ||
defaultOrganization
)
);
});
}
if (node.promptValues?.instance_groups?.length > 0)
/* eslint-disable no-await-in-loop, no-restricted-syntax */
for (const group of node.promptValues.instance_groups) {
instanceGroupRequests.push(
WorkflowJobTemplateNodesAPI.associateInstanceGroup(
data.id,
group.id
)
);
}
}) })
); );
} }
@@ -487,6 +536,8 @@ function Visualizer({ template }) {
nodeRequests.push( nodeRequests.push(
WorkflowJobTemplateNodesAPI.replace(node.originalNodeObject.id, { WorkflowJobTemplateNodesAPI.replace(node.originalNodeObject.id, {
...node.promptValues, ...node.promptValues,
execution_environment:
node.promptValues?.execution_environment?.id || null,
inventory: node.promptValues?.inventory?.id || null, inventory: node.promptValues?.inventory?.id || null,
unified_job_template: node.fullUnifiedJobTemplate.id, unified_job_template: node.fullUnifiedJobTemplate.id,
all_parents_must_converge: node.all_parents_must_converge, all_parents_must_converge: node.all_parents_must_converge,
@@ -503,6 +554,12 @@ function Visualizer({ template }) {
node.promptValues?.credentials node.promptValues?.credentials
); );
const { added: addedLabels, removed: removedLabels } =
getAddedAndRemoved(
node?.originalNodeLabels,
node.promptValues?.labels
);
if (addedCredentials.length > 0) { if (addedCredentials.length > 0) {
addedCredentials.forEach((cred) => { addedCredentials.forEach((cred) => {
associateCredentialRequests.push( associateCredentialRequests.push(
@@ -523,6 +580,41 @@ function Visualizer({ template }) {
) )
); );
} }
if (addedLabels.length > 0) {
addedLabels.forEach((label) => {
associateLabelRequests.push(
WorkflowJobTemplateNodesAPI.associateLabel(
node.originalNodeObject.id,
label,
node.fullUnifiedJobTemplate.organization ||
defaultOrganization
)
);
});
}
if (removedLabels?.length > 0) {
removedLabels.forEach((label) =>
disassociateLabelRequests.push(
WorkflowJobTemplateNodesAPI.disassociateLabel(
node.originalNodeObject.id,
label,
node.fullUnifiedJobTemplate.organization ||
defaultOrganization
)
)
);
}
if (node.promptValues?.instance_groups) {
instanceGroupRequests.push(
WorkflowJobTemplateNodesAPI.orderInstanceGroups(
node.originalNodeObject.id,
node.promptValues?.instance_groups,
node?.originalNodeInstanceGroups || []
)
);
}
}) })
); );
} }
@@ -539,11 +631,18 @@ function Visualizer({ template }) {
); );
await Promise.all(associateNodes(newLinks, originalLinkMap)); await Promise.all(associateNodes(newLinks, originalLinkMap));
await Promise.all(disassociateCredentialRequests); await Promise.all([
await Promise.all(associateCredentialRequests); ...disassociateCredentialRequests,
...disassociateLabelRequests,
]);
await Promise.all([
...associateCredentialRequests,
...associateLabelRequests,
...instanceGroupRequests,
]);
history.push(`/templates/workflow_job_template/${template.id}/details`); history.push(`/templates/workflow_job_template/${template.id}/details`);
}, [links, nodes, history, template.id]), }, [links, nodes, history, defaultOrganization, template.id]),
{} {}
); );

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { import {
OrganizationsAPI,
WorkflowApprovalTemplatesAPI, WorkflowApprovalTemplatesAPI,
WorkflowJobTemplateNodesAPI, WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI, WorkflowJobTemplatesAPI,
@@ -104,6 +105,12 @@ const mockWorkflowNodes = [
describe('Visualizer', () => { describe('Visualizer', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,
results: [{ id: 1, name: 'Default' }],
},
});
WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({
data: { data: {
count: mockWorkflowNodes.length, count: mockWorkflowNodes.length,

View File

@@ -64,7 +64,6 @@ 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 [detailError, setDetailError] = useState(null);
const dispatch = useContext(WorkflowDispatchContext); const dispatch = useContext(WorkflowDispatchContext);
const { addingLink, addLinkSourceNode, nodePositions, nodes } = const { addingLink, addLinkSourceNode, nodePositions, nodes } =
@@ -72,7 +71,6 @@ function VisualizerNode({
const isAddLinkSourceNode = const isAddLinkSourceNode =
addLinkSourceNode && addLinkSourceNode.id === node.id; addLinkSourceNode && addLinkSourceNode.id === node.id;
const handleCredentialsErrorClose = () => setCredentialsError(null);
const handleDetailErrorClose = () => setDetailError(null); const handleDetailErrorClose = () => setDetailError(null);
const updateNode = async () => { const updateNode = async () => {
@@ -98,18 +96,47 @@ function VisualizerNode({
if ( if (
node?.originalNodeObject?.summary_fields?.unified_job_template node?.originalNodeObject?.summary_fields?.unified_job_template
?.unified_job_type === 'job' && ?.unified_job_type === 'job' ||
!node?.originalNodeCredentials node?.originalNodeObject?.summary_fields?.unified_job_template
?.unified_job_type === 'workflow_job'
) { ) {
try { try {
const { if (
data: { results }, node?.originalNodeObject?.summary_fields?.unified_job_template
} = await WorkflowJobTemplateNodesAPI.readCredentials( ?.unified_job_type === 'job' &&
node.originalNodeObject.id !node?.originalNodeCredentials
); ) {
updatedNode.originalNodeCredentials = results; const {
data: { results },
} = await WorkflowJobTemplateNodesAPI.readCredentials(
node.originalNodeObject.id
);
updatedNode.originalNodeCredentials = results;
}
if (
node?.originalNodeObject?.summary_fields?.unified_job_template
?.unified_job_type === 'job' &&
!node.originalNodeLabels
) {
const {
data: { results },
} = await WorkflowJobTemplateNodesAPI.readAllLabels(
node.originalNodeObject.id
);
updatedNode.originalNodeLabels = results;
updatedNode.originalNodeObject.labels = results;
}
if (!node.originalNodeInstanceGroups) {
const {
data: { results },
} = await WorkflowJobTemplateNodesAPI.readInstanceGroups(
node.originalNodeObject.id
);
updatedNode.originalNodeInstanceGroups = results;
updatedNode.originalNodeObject.instance_groups = results;
}
} catch (err) { } catch (err) {
setCredentialsError(err); setDetailError(err);
return null; return null;
} }
} }
@@ -350,17 +377,6 @@ function VisualizerNode({
<ErrorDetail error={detailError} /> <ErrorDetail error={detailError} />
</AlertModal> </AlertModal>
)} )}
{credentialsError && (
<AlertModal
isOpen={credentialsError}
variant="error"
title={t`Error!`}
onClose={handleCredentialsErrorClose}
>
{t`Failed to retrieve node credentials.`}
<ErrorDetail error={credentialsError} />
</AlertModal>
)}
</> </>
); );
} }

View File

@@ -8,9 +8,9 @@ const wfHelpTextStrings = () => ({
playbook. Multiple patterns are allowed. Refer to Ansible playbook. Multiple patterns are allowed. Refer to Ansible
documentation for more information and examples on patterns.`, documentation for more information and examples on patterns.`,
sourceControlBranch: t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.`, sourceControlBranch: t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.`,
labels: t`Optional labels that describe this job template, labels: t`Optional labels that describe this workflow job template,
such as 'dev' or 'test'. Labels can be used to group and filter such as 'dev' or 'test'. Labels can be used to group and filter
job templates and completed jobs.`, workflow job templates and completed jobs.`,
variables: t`Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Controller documentation for example syntax.`, variables: t`Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Controller documentation for example syntax.`,
enableWebhook: t`Enable Webhook for this workflow job template.`, enableWebhook: t`Enable Webhook for this workflow job template.`,
enableConcurrentJobs: t`If enabled, simultaneous runs of this workflow job template will be allowed.`, enableConcurrentJobs: t`If enabled, simultaneous runs of this workflow job template will be allowed.`,
@@ -18,6 +18,7 @@ const wfHelpTextStrings = () => ({
webhookKey: t`Webhook services can use this as a shared secret.`, webhookKey: t`Webhook services can use this as a shared secret.`,
webhookCredential: t`Optionally select the credential to use to send status updates back to the webhook service.`, webhookCredential: t`Optionally select the credential to use to send status updates back to the webhook service.`,
webhookService: t`Select a webhook service.`, webhookService: t`Select a webhook service.`,
jobTags: t`Tags are useful when you have a large playbook, and you want to run a specific part of a play or task. Use commas to separate multiple tags. Refer to the documentation for details on the usage of tags.`,
skipTags: t`Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task. Use commas to separate multiple tags. Refer to the documentation for details on the usage of tags.`, skipTags: t`Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task. Use commas to separate multiple tags. Refer to the documentation for details on the usage of tags.`,
enabledOptions: ( enabledOptions: (
<> <>

View File

@@ -211,8 +211,6 @@ function WorkflowJobTemplateForm({
promptId="template-ask-variables-on-launch" promptId="template-ask-variables-on-launch"
tooltip={helpText.variables} tooltip={helpText.variables}
/> />
</FormFullWidthLayout>
<FormColumnLayout>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-tags" fieldId="template-tags"
label={t`Job Tags`} label={t`Job Tags`}
@@ -237,7 +235,7 @@ function WorkflowJobTemplateForm({
onChange={(value) => skipTagsHelpers.setValue(value)} onChange={(value) => skipTagsHelpers.setValue(value)}
/> />
</FieldWithPrompt> </FieldWithPrompt>
</FormColumnLayout> </FormFullWidthLayout>
<FormGroup fieldId="options" label={t`Options`}> <FormGroup fieldId="options" label={t`Options`}>
<FormCheckboxLayout isInline> <FormCheckboxLayout isInline>
<Checkbox <Checkbox