Adds support for workflow node aliasing via identifier field

This commit is contained in:
mabashian
2021-07-02 16:07:55 -04:00
parent 791d24bcb6
commit 231cccbb19
16 changed files with 310 additions and 153 deletions

View File

@@ -6,6 +6,7 @@ import styled from 'styled-components';
import { ExclamationTriangleIcon } from '@patternfly/react-icons'; import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import { shape } from 'prop-types'; import { shape } from 'prop-types';
import { secondsToHHMMSS } from 'util/dates'; import { secondsToHHMMSS } from 'util/dates';
import { stringIsUUID } from 'util/strings';
const GridDL = styled.dl` const GridDL = styled.dl`
column-gap: 15px; column-gap: 15px;
@@ -37,6 +38,11 @@ function WorkflowNodeHelp({ node }) {
const unifiedJobTemplate = const unifiedJobTemplate =
node?.fullUnifiedJobTemplate || node?.fullUnifiedJobTemplate ||
node?.originalNodeObject?.summary_fields?.unified_job_template; node?.originalNodeObject?.summary_fields?.unified_job_template;
const identifier =
node?.identifier ||
(!stringIsUUID(node?.originalNodeObject?.identifier)
? node.originalNodeObject.identifier
: null);
if (unifiedJobTemplate || job) { if (unifiedJobTemplate || job) {
const type = unifiedJobTemplate const type = unifiedJobTemplate
? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type ? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type
@@ -132,10 +138,18 @@ function WorkflowNodeHelp({ node }) {
)} )}
{job && ( {job && (
<GridDL> <GridDL>
{identifier && (
<>
<dt>
<b>{t`Node Alias`}</b>
</dt>
<dd id="workflow-node-help-alias">{identifier}</dd>
</>
)}
<dt> <dt>
<b>{t`Name`}</b> <b>{t`Resource Name`}</b>
</dt> </dt>
<dd id="workflow-node-help-name">{job.name}</dd> <dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd>
<dt> <dt>
<b>{t`Type`}</b> <b>{t`Type`}</b>
</dt> </dt>
@@ -158,8 +172,16 @@ function WorkflowNodeHelp({ node }) {
)} )}
{unifiedJobTemplate && !job && ( {unifiedJobTemplate && !job && (
<GridDL> <GridDL>
{identifier && (
<>
<dt>
<b>{t`Node Alias`}</b>
</dt>
<dd id="workflow-node-help-alias">{identifier}</dd>
</>
)}
<dt> <dt>
<b>{t`Name`}</b> <b>{t`Resource Name`}</b>
</dt> </dt>
<dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd> <dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd>
<dt> <dt>

View File

@@ -2,29 +2,32 @@ import React from 'react';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import WorkflowNodeHelp from './WorkflowNodeHelp'; import WorkflowNodeHelp from './WorkflowNodeHelp';
describe('WorkflowNodeHelp', () => { const node = {
test('successfully mounts', () => { originalNodeObject: {
const wrapper = mountWithContexts(<WorkflowNodeHelp node={{}} />); identifier: 'Foo',
expect(wrapper).toHaveLength(1); summary_fields: {
}); job: {
test('renders the expected content for a completed job template job', () => {
const node = {
originalNodeObject: {
summary_fields: {
job: {
name: 'Foo Job Template',
elapsed: 9000,
status: 'successful',
type: 'job',
},
},
},
unifiedJobTemplate: {
name: 'Foo Job Template', name: 'Foo Job Template',
unified_job_type: 'job', elapsed: 9000,
status: 'successful',
type: 'job',
}, },
}; unified_job_template: {
name: 'Foo Job Template',
type: 'job_template',
},
},
},
unifiedJobTemplate: {
name: 'Foo Job Template',
unified_job_type: 'job',
},
};
describe('WorkflowNodeHelp', () => {
test('renders the expected content for a completed job template job', () => {
const wrapper = mountWithContexts(<WorkflowNodeHelp node={node} />); const wrapper = mountWithContexts(<WorkflowNodeHelp node={node} />);
expect(wrapper.find('#workflow-node-help-alias').text()).toBe('Foo');
expect(wrapper.find('#workflow-node-help-name').text()).toBe( expect(wrapper.find('#workflow-node-help-name').text()).toBe(
'Foo Job Template' 'Foo Job Template'
); );

View File

@@ -184,6 +184,7 @@ function createNode(state, node) {
isInvalidLinkTarget: false, isInvalidLinkTarget: false,
promptValues: node.promptValues, promptValues: node.promptValues,
all_parents_must_converge: node.all_parents_must_converge, all_parents_must_converge: node.all_parents_must_converge,
identifier: node.identifier,
}); });
// Ensures that root nodes appear to always run // Ensures that root nodes appear to always run
@@ -660,17 +661,16 @@ function updateNode(state, editedNode) {
launchConfig, launchConfig,
promptValues, promptValues,
all_parents_must_converge, all_parents_must_converge,
identifier,
} = editedNode; } = 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.all_parents_must_converge = all_parents_must_converge; matchingNode.all_parents_must_converge = all_parents_must_converge;
if (matchingNode.originalNodeObject) {
delete matchingNode.originalNodeObject.all_parents_must_converge;
}
matchingNode.fullUnifiedJobTemplate = nodeResource; matchingNode.fullUnifiedJobTemplate = nodeResource;
matchingNode.isEdited = true; matchingNode.isEdited = true;
matchingNode.launchConfig = launchConfig; matchingNode.launchConfig = launchConfig;
matchingNode.identifier = identifier;
if (promptValues) { if (promptValues) {
matchingNode.promptValues = promptValues; matchingNode.promptValues = promptValues;

View File

@@ -65,6 +65,7 @@ const workflowContext = {
{ {
id: 2, id: 2,
originalNodeObject: { originalNodeObject: {
identifier: 'Node identifier',
summary_fields: { summary_fields: {
job: { job: {
name: 'Foo JT', name: 'Foo JT',
@@ -72,6 +73,10 @@ const workflowContext = {
status: 'successful', status: 'successful',
elapsed: 60, elapsed: 60,
}, },
unified_job_template: {
name: 'Foo JT',
type: 'job_template',
},
}, },
}, },
}, },
@@ -185,9 +190,17 @@ describe('WorkflowOutputGraph', () => {
expect(wrapper.find('WorkflowLinkHelp')).toHaveLength(0); expect(wrapper.find('WorkflowLinkHelp')).toHaveLength(0);
wrapper.find('g#node-2').simulate('mouseenter'); wrapper.find('g#node-2').simulate('mouseenter');
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1); expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1);
expect(wrapper.find('WorkflowNodeHelp').contains(<b>Name</b>)).toEqual( expect(
true wrapper.find('WorkflowNodeHelp').contains(<b>Node Alias</b>)
); ).toEqual(true);
expect(
wrapper
.find('WorkflowNodeHelp')
.containsMatchingElement(<dd>Node identifier</dd>)
).toEqual(true);
expect(
wrapper.find('WorkflowNodeHelp').contains(<b>Resource Name</b>)
).toEqual(true);
expect( expect(
wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>) wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>)
).toEqual(true); ).toEqual(true);

View File

@@ -8,6 +8,7 @@ import { WorkflowStateContext } from 'contexts/Workflow';
import StatusIcon from 'components/StatusIcon'; import StatusIcon from 'components/StatusIcon';
import { WorkflowNodeTypeLetter } from 'components/Workflow'; import { WorkflowNodeTypeLetter } from 'components/Workflow';
import { secondsToHHMMSS } from 'util/dates'; import { secondsToHHMMSS } from 'util/dates';
import { stringIsUUID } from 'util/strings';
import { constants as wfConstants } from 'components/Workflow/WorkflowUtils'; import { constants as wfConstants } from 'components/Workflow/WorkflowUtils';
const NodeG = styled.g` const NodeG = styled.g`
@@ -67,9 +68,6 @@ function WorkflowOutputNode({ 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';
@@ -94,6 +92,23 @@ function WorkflowOutputNode({ mouseEnter, mouseLeave, node }) {
} }
}; };
let nodeName;
if (
node?.identifier ||
(node?.originalNodeObject?.identifier &&
!stringIsUUID(node.originalNodeObject.identifier))
) {
nodeName = node?.identifier
? node?.identifier
: node?.originalNodeObject?.identifier;
} else {
nodeName =
node?.fullUnifiedJobTemplate?.name ||
node?.originalNodeObject?.summary_fields?.unified_job_template?.name ||
t`DELETED`;
}
return ( return (
<NodeG <NodeG
id={`node-${node.id}`} id={`node-${node.id}`}
@@ -144,14 +159,14 @@ function WorkflowOutputNode({ mouseEnter, mouseLeave, node }) {
<> <>
<JobTopLine> <JobTopLine>
{job.status !== 'pending' && <StatusIcon status={job.status} />} {job.status !== 'pending' && <StatusIcon status={job.status} />}
<p>{jobName}</p> <p>{nodeName}</p>
</JobTopLine> </JobTopLine>
{!!job?.elapsed && ( {!!job?.elapsed && (
<Elapsed>{secondsToHHMMSS(job.elapsed)}</Elapsed> <Elapsed>{secondsToHHMMSS(job.elapsed)}</Elapsed>
)} )}
</> </>
) : ( ) : (
<NodeDefaultLabel>{jobName || t`DELETED`}</NodeDefaultLabel> <NodeDefaultLabel>{nodeName}</NodeDefaultLabel>
)} )}
</NodeContents> </NodeContents>
</foreignObject> </foreignObject>

View File

@@ -102,7 +102,6 @@ function updateNode(nodes, index, message) {
job: { job: {
...nodes[index]?.job, ...nodes[index]?.job,
id: message.unified_job_id, id: message.unified_job_id,
name: nodes[index]?.job?.name || nodes[index]?.unifiedJobTemplate?.name,
status: message.status, status: message.status,
type: message.type, type: message.type,
}, },

View File

@@ -20,6 +20,7 @@ function NodeAddModal() {
timeoutSeconds, timeoutSeconds,
linkType, linkType,
convergence, convergence,
identifier,
} = values; } = values;
if (values) { if (values) {
@@ -35,6 +36,7 @@ function NodeAddModal() {
const node = { const node = {
linkType, linkType,
all_parents_must_converge: convergence === 'all', all_parents_must_converge: convergence === 'all',
identifier,
}; };
delete values.convergence; delete values.convergence;

View File

@@ -18,6 +18,7 @@ function NodeEditModal() {
timeoutMinutes, timeoutMinutes,
timeoutSeconds, timeoutSeconds,
convergence, convergence,
identifier,
...rest ...rest
} = values; } = values;
let node; let node;
@@ -30,11 +31,13 @@ function NodeEditModal() {
timeout: Number(timeoutMinutes) * 60 + Number(timeoutSeconds), timeout: Number(timeoutMinutes) * 60 + Number(timeoutSeconds),
type: 'workflow_approval_template', type: 'workflow_approval_template',
}, },
identifier,
}; };
} else { } else {
node = { node = {
nodeResource, nodeResource,
all_parents_must_converge: convergence === 'all', all_parents_must_converge: convergence === 'all',
identifier,
}; };
if (nodeType === 'job_template' || nodeType === 'workflow_job_template') { if (nodeType === 'job_template' || nodeType === 'workflow_job_template') {
node.promptValues = { node.promptValues = {

View File

@@ -536,6 +536,7 @@ describe('Edit existing node', () => {
value={{ value={{
nodeToEdit: { nodeToEdit: {
id: 2, id: 2,
identifier: 'Foo',
fullUnifiedJobTemplate: { fullUnifiedJobTemplate: {
id: 1, id: 1,
name: 'Test Project', name: 'Test Project',
@@ -602,6 +603,7 @@ describe('Edit existing node', () => {
expect(onSave).toBeCalledWith( expect(onSave).toBeCalledWith(
{ {
convergence: 'any', convergence: 'any',
identifier: 'Foo',
approvalDescription: 'Test Approval Description', approvalDescription: 'Test Approval Description',
approvalName: 'Test Approval', approvalName: 'Test Approval',
linkType: 'success', linkType: 'success',
@@ -622,6 +624,7 @@ describe('Edit existing node', () => {
value={{ value={{
nodeToEdit: { nodeToEdit: {
id: 2, id: 2,
identifier: 'Foo',
fullUnifiedJobTemplate: { fullUnifiedJobTemplate: {
id: 1, id: 1,
name: 'Test Approval', name: 'Test Approval',
@@ -672,6 +675,7 @@ describe('Edit existing node', () => {
expect(onSave).toBeCalledWith( expect(onSave).toBeCalledWith(
{ {
convergence: 'any', convergence: 'any',
identifier: 'Foo',
linkType: 'success', linkType: 'success',
nodeResource: { nodeResource: {
id: 1, id: 1,

View File

@@ -31,7 +31,7 @@ const NodeTypeErrorAlert = styled(Alert)`
`; `;
const TimeoutInput = styled(TextInput)` const TimeoutInput = styled(TextInput)`
width: 200px; width: 200px !important;
:not(:first-of-type) { :not(:first-of-type) {
margin-left: 20px; margin-left: 20px;
} }
@@ -43,7 +43,7 @@ const TimeoutLabel = styled.p`
min-width: fit-content; min-width: fit-content;
`; `;
function NodeTypeStep() { function NodeTypeStep({ isIdentifierRequired }) {
const [nodeTypeField, , nodeTypeHelpers] = useField('nodeType'); const [nodeTypeField, , nodeTypeHelpers] = useField('nodeType');
const [nodeResourceField, nodeResourceMeta, nodeResourceHelpers] = const [nodeResourceField, nodeResourceMeta, nodeResourceHelpers] =
useField('nodeResource'); useField('nodeResource');
@@ -157,105 +157,117 @@ function NodeTypeStep() {
)} )}
<Form css="margin-top: 20px;"> <Form css="margin-top: 20px;">
<FormColumnLayout> <FormColumnLayout>
{nodeTypeField.value === 'workflow_approval_template' && ( <FormFullWidthLayout>
<FormFullWidthLayout> {nodeTypeField.value === 'workflow_approval_template' && (
<FormField <>
name="approvalName" <FormField
id="approval-name" name="approvalName"
isRequired id="approval-name"
validate={required(null)} isRequired
validated={isValid ? 'default' : 'error'} validate={required(null)}
label={t`Name`} validated={isValid ? 'default' : 'error'}
/> label={t`Name`}
<FormField />
name="approvalDescription" <FormField
id="approval-description" name="approvalDescription"
label={t`Description`} id="approval-description"
/> label={t`Description`}
<FormGroup />
label={t`Timeout`} <FormGroup
fieldId="approval-timeout" label={t`Timeout`}
name="timeout" fieldId="approval-timeout"
> name="timeout"
<div css="display: flex;align-items: center;"> >
<TimeoutInput <div css="display: flex;align-items: center;">
{...timeoutMinutesField} <TimeoutInput
aria-label={t`Timeout minutes`} {...timeoutMinutesField}
id="approval-timeout-minutes" aria-label={t`Timeout minutes`}
min="0" id="approval-timeout-minutes"
onChange={(value, event) => { min="0"
timeoutMinutesField.onChange(event); onChange={(value, event) => {
}} timeoutMinutesField.onChange(event);
step="1" }}
type="number" step="1"
/> type="number"
<TimeoutLabel> />
<Trans>min</Trans> <TimeoutLabel>
</TimeoutLabel> <Trans>min</Trans>
<TimeoutInput </TimeoutLabel>
{...timeoutSecondsField} <TimeoutInput
aria-label={t`Timeout seconds`} {...timeoutSecondsField}
id="approval-timeout-seconds" aria-label={t`Timeout seconds`}
min="0" id="approval-timeout-seconds"
onChange={(value, event) => { min="0"
timeoutSecondsField.onChange(event); onChange={(value, event) => {
}} timeoutSecondsField.onChange(event);
step="1" }}
type="number" step="1"
/> type="number"
<TimeoutLabel> />
<Trans>sec</Trans> <TimeoutLabel>
</TimeoutLabel> <Trans>sec</Trans>
</div> </TimeoutLabel>
</FormGroup> </div>
</FormFullWidthLayout> </FormGroup>
)} </>
<FormGroup )}
fieldId="convergence" <FormGroup
label={t`Convergence`} fieldId="convergence"
isRequired label={t`Convergence`}
labelIcon={ isRequired
<Popover labelIcon={
content={ <Popover
<> content={
{t`Preconditions for running this node when there are multiple parents. Refer to the`}{' '} <>
<a {t`Preconditions for running this node when there are multiple parents. Refer to the`}{' '}
href={`${getDocsBaseUrl( <a
config href={`${getDocsBaseUrl(
)}/html/userguide/workflow_templates.html#convergence-node`} config
target="_blank" )}/html/userguide/workflow_templates.html#convergence-node`}
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
{t`documentation`} >
</a>{' '} {t`documentation`}
{t`for more info.`} </a>{' '}
</> {t`for more info.`}
} </>
/> }
} />
> }
<Select
variant={SelectVariant.single}
isOpen={isConvergenceOpen}
selections={convergenceField.value}
onToggle={setIsConvergenceOpen}
onSelect={(event, selection) => {
convergenceFieldHelpers.setValue(selection);
setIsConvergenceOpen(false);
}}
aria-label={t`Convergence select`}
typeAheadAriaLabel={t`Convergence select`}
className="convergenceSelect"
ouiaId="convergenceSelect"
> >
<SelectOption key="any" value="any" id="select-option-any"> <Select
{t`Any`} variant={SelectVariant.single}
</SelectOption> isOpen={isConvergenceOpen}
<SelectOption key="all" value="all" id="select-option-all"> selections={convergenceField.value}
{t`All`} onToggle={setIsConvergenceOpen}
</SelectOption> onSelect={(event, selection) => {
</Select> convergenceFieldHelpers.setValue(selection);
</FormGroup> setIsConvergenceOpen(false);
}}
aria-label={t`Convergence select`}
typeAheadAriaLabel={t`Convergence select`}
className="convergenceSelect"
ouiaId="convergenceSelect"
>
<SelectOption key="any" value="any" id="select-option-any">
{t`Any`}
</SelectOption>
<SelectOption key="all" value="all" id="select-option-all">
{t`All`}
</SelectOption>
</Select>
</FormGroup>
<FormField
id="node-alias"
name="identifier"
aria-label={t`Node Alias`}
label={t`Node Alias`}
tooltip={t`If specified, this field will be shown on the node instead of the resource name when viewing the workflow`}
isRequired={isIdentifierRequired}
validate={isIdentifierRequired ? required(null) : null}
validated={isValid ? 'default' : 'error'}
/>
</FormFullWidthLayout>
</FormColumnLayout> </FormColumnLayout>
</Form> </Form>
</> </>

View File

@@ -2,14 +2,16 @@ import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useField } from 'formik'; import { useField } from 'formik';
import StepName from 'components/LaunchPrompt/steps/StepName'; import StepName from 'components/LaunchPrompt/steps/StepName';
import { stringIsUUID } from 'util/strings';
import NodeTypeStep from './NodeTypeStep'; import NodeTypeStep from './NodeTypeStep';
const STEP_ID = 'nodeType'; const STEP_ID = 'nodeType';
export default function useNodeTypeStep() { export default function useNodeTypeStep(nodeToEdit) {
const [, meta] = useField('nodeType'); const [, meta] = useField('nodeType');
const [approvalNameField] = useField('approvalName'); const [approvalNameField] = useField('approvalName');
const [nodeTypeField, ,] = useField('nodeType'); const [nodeTypeField, ,] = useField('nodeType');
const [, identifierMeta] = useField('identifier');
const [nodeResourceField, nodeResourceMeta] = useField({ const [nodeResourceField, nodeResourceMeta] = useField({
name: 'nodeResource', name: 'nodeResource',
validate: (value) => { validate: (value) => {
@@ -26,14 +28,16 @@ export default function useNodeTypeStep() {
}, },
}); });
const formError = !!meta.error || !!nodeResourceMeta.error; const formError =
!!meta.error || !!nodeResourceMeta.error || identifierMeta.error;
return { return {
step: getStep( step: getStep(
nodeTypeField, nodeTypeField,
approvalNameField, approvalNameField,
nodeResourceField, nodeResourceField,
formError formError,
nodeToEdit
), ),
initialValues: getInitialValues(), initialValues: getInitialValues(),
isReady: true, isReady: true,
@@ -49,7 +53,8 @@ function getStep(
nodeTypeField, nodeTypeField,
approvalNameField, approvalNameField,
nodeResourceField, nodeResourceField,
formError formError,
nodeToEdit
) { ) {
const isEnabled = () => { const isEnabled = () => {
if ( if (
@@ -70,7 +75,15 @@ function getStep(
{t`Node type`} {t`Node type`}
</StepName> </StepName>
), ),
component: <NodeTypeStep />, component: (
<NodeTypeStep
isIdentifierRequired={
nodeToEdit &&
nodeToEdit.originalNodeObject &&
!stringIsUUID(nodeToEdit.originalNodeObject?.identifier)
}
/>
),
enableNext: isEnabled(), enableNext: isEnabled(),
}; };
} }
@@ -83,5 +96,6 @@ function getInitialValues() {
timeoutSeconds: 0, timeoutSeconds: 0,
nodeType: 'job_template', nodeType: 'job_template',
convergence: 'any', convergence: 'any',
identifier: '',
}; };
} }

View File

@@ -8,6 +8,7 @@ import useSurveyStep from 'components/LaunchPrompt/steps/useSurveyStep';
import usePreviewStep from 'components/LaunchPrompt/steps/usePreviewStep'; import usePreviewStep from 'components/LaunchPrompt/steps/usePreviewStep';
import { WorkflowStateContext } from 'contexts/Workflow'; import { WorkflowStateContext } from 'contexts/Workflow';
import { jsonToYaml } from 'util/yaml'; import { jsonToYaml } from 'util/yaml';
import { stringIsUUID } from 'util/strings';
import useNodeTypeStep from './NodeTypeStep/useNodeTypeStep'; import useNodeTypeStep from './NodeTypeStep/useNodeTypeStep';
import useDaysToKeepStep from './useDaysToKeepStep'; import useDaysToKeepStep from './useDaysToKeepStep';
import useRunTypeStep from './useRunTypeStep'; import useRunTypeStep from './useRunTypeStep';
@@ -37,6 +38,22 @@ const getNodeToEditDefaultValues = (
nodeToEdit, nodeToEdit,
resourceDefaultCredentials resourceDefaultCredentials
) => { ) => {
let identifier = '';
if (
Object.prototype.hasOwnProperty.call(nodeToEdit, 'identifier') &&
nodeToEdit.identifier !== null
) {
({ identifier } = nodeToEdit);
} else if (
Object.prototype.hasOwnProperty.call(
nodeToEdit?.originalNodeObject,
'identifier'
) &&
!stringIsUUID(nodeToEdit?.originalNodeObject?.identifier)
) {
identifier = nodeToEdit?.originalNodeObject?.identifier;
}
const initialValues = { const initialValues = {
nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null, nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null,
nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template', nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template',
@@ -45,6 +62,7 @@ const getNodeToEditDefaultValues = (
nodeToEdit?.originalNodeObject?.all_parents_must_converge nodeToEdit?.originalNodeObject?.all_parents_must_converge
? 'all' ? 'all'
: 'any', : 'any',
identifier,
}; };
if ( if (
@@ -274,6 +292,8 @@ export default function useWorkflowNodeSteps(
}), }),
{} {}
); );
initialValues.identifier = formikValues.identifier;
initialValues.convergence = formikValues.convergence;
} }
const errors = formikErrors.nodeResource const errors = formikErrors.nodeResource
@@ -289,15 +309,10 @@ export default function useWorkflowNodeSteps(
errors.nodeResource = t`Job Templates with credentials that prompt for passwords cannot be selected when creating or editing nodes`; errors.nodeResource = t`Job Templates with credentials that prompt for passwords cannot be selected when creating or editing nodes`;
} }
if (initialValues.convergence === 'all') {
formikValues.convergence = 'all';
}
resetForm({ resetForm({
errors, errors,
values: { values: {
...initialValues, ...initialValues,
convergence: formikValues.convergence,
nodeResource: formikValues.nodeResource, nodeResource: formikValues.nodeResource,
nodeType: formikValues.nodeType, nodeType: formikValues.nodeType,
linkType: formikValues.linkType, linkType: formikValues.linkType,

View File

@@ -9,6 +9,7 @@ import {
WorkflowStateContext, WorkflowStateContext,
} from 'contexts/Workflow'; } from 'contexts/Workflow';
import { getAddedAndRemoved } from 'util/lists'; import { getAddedAndRemoved } from 'util/lists';
import { stringIsUUID } from 'util/strings';
import AlertModal from 'components/AlertModal'; import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail'; import ErrorDetail from 'components/ErrorDetail';
import { layoutGraph } from 'components/Workflow/WorkflowUtils'; import { layoutGraph } from 'components/Workflow/WorkflowUtils';
@@ -51,6 +52,20 @@ const Wrapper = styled.div`
height: 100%; height: 100%;
`; `;
const replaceIdentifier = node => {
if (stringIsUUID(node.originalNodeObject.identifier) && node.identifier) {
return true;
}
if (
!stringIsUUID(node.originalNodeObject.identifier) &&
node.identifier !== node.originalNodeObject.identifier
) {
return true;
}
return false;
};
const getAggregatedCredentials = ( const getAggregatedCredentials = (
originalNodeOverride = [], originalNodeOverride = [],
templateDefaultCredentials = [] templateDefaultCredentials = []
@@ -366,6 +381,7 @@ function Visualizer({ template }) {
nodeRequests.push( nodeRequests.push(
WorkflowJobTemplatesAPI.createNode(template.id, { WorkflowJobTemplatesAPI.createNode(template.id, {
all_parents_must_converge: node.all_parents_must_converge, all_parents_must_converge: node.all_parents_must_converge,
...(node.identifier && { identifier: node.identifier }),
}).then(({ data }) => { }).then(({ data }) => {
node.originalNodeObject = data; node.originalNodeObject = data;
originalLinkMap[node.id] = { originalLinkMap[node.id] = {
@@ -390,6 +406,7 @@ function Visualizer({ template }) {
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,
...(node.identifier && { identifier: node.identifier }),
}).then(({ data }) => { }).then(({ data }) => {
node.originalNodeObject = data; node.originalNodeObject = data;
originalLinkMap[node.id] = { originalLinkMap[node.id] = {
@@ -425,6 +442,9 @@ function Visualizer({ template }) {
node.originalNodeObject.id, node.originalNodeObject.id,
{ {
all_parents_must_converge: node.all_parents_must_converge, all_parents_must_converge: node.all_parents_must_converge,
...(replaceIdentifier(node) && {
identifier: node.identifier,
}),
} }
).then(({ data }) => { ).then(({ data }) => {
node.originalNodeObject = data; node.originalNodeObject = data;
@@ -447,6 +467,9 @@ function Visualizer({ template }) {
node.originalNodeObject.id, node.originalNodeObject.id,
{ {
all_parents_must_converge: node.all_parents_must_converge, all_parents_must_converge: node.all_parents_must_converge,
...(replaceIdentifier(node) && {
identifier: node.identifier,
}),
} }
).then(({ data }) => { ).then(({ data }) => {
node.originalNodeObject = data; node.originalNodeObject = data;
@@ -470,6 +493,9 @@ function Visualizer({ template }) {
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,
...(replaceIdentifier(node) && {
identifier: node.identifier,
}),
}).then(() => { }).then(() => {
const { added: addedCredentials, removed: removedCredentials } = const { added: addedCredentials, removed: removedCredentials } =
getAddedAndRemoved( getAddedAndRemoved(

View File

@@ -68,15 +68,19 @@ const workflowContext = {
name: 'Foo JT', name: 'Foo JT',
type: 'job_template', type: 'job_template',
}, },
identifier: 'node 2',
}, },
{ {
id: 3, id: 3,
identifier: 'node 3',
}, },
{ {
id: 4, id: 4,
identifier: 'node 4',
}, },
{ {
id: 5, id: 5,
identifier: 'node 5',
}, },
], ],
showLegend: false, showLegend: false,
@@ -183,9 +187,15 @@ describe('VisualizerGraph', () => {
.first() .first()
.simulate('mouseenter'); .simulate('mouseenter');
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1); expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1);
expect(wrapper.find('WorkflowNodeHelp').contains(<b>Name</b>)).toEqual( expect(
true wrapper.find('WorkflowNodeHelp').contains(<b>Node Alias</b>)
); ).toEqual(true);
expect(
wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>node 2</dd>)
).toEqual(true);
expect(
wrapper.find('WorkflowNodeHelp').contains(<b>Resource Name</b>)
).toEqual(true);
expect( expect(
wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>) wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>)
).toEqual(true); ).toEqual(true);

View File

@@ -1,6 +1,5 @@
import React, { useContext, useRef, useState } from 'react'; import React, { useContext, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { bool, func, shape } from 'prop-types'; import { bool, func, shape } from 'prop-types';
import { import {
@@ -18,6 +17,7 @@ import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail'; import ErrorDetail from 'components/ErrorDetail';
import { WorkflowJobTemplateNodesAPI } from 'api'; import { WorkflowJobTemplateNodesAPI } from 'api';
import { constants as wfConstants } from 'components/Workflow/WorkflowUtils'; import { constants as wfConstants } from 'components/Workflow/WorkflowUtils';
import { stringIsUUID } from 'util/strings';
import { import {
WorkflowActionTooltip, WorkflowActionTooltip,
WorkflowActionTooltipItem, WorkflowActionTooltipItem,
@@ -168,6 +168,23 @@ function VisualizerNode({
} }
}; };
let nodeName;
if (
node?.identifier ||
(node?.originalNodeObject?.identifier &&
!stringIsUUID(node.originalNodeObject.identifier))
) {
nodeName = node?.identifier
? node?.identifier
: node?.originalNodeObject?.identifier;
} else {
nodeName =
node?.fullUnifiedJobTemplate?.name ||
node?.originalNodeObject?.summary_fields?.unified_job_template?.name ||
t`DELETED`;
}
const viewDetailsAction = ( const viewDetailsAction = (
<WorkflowActionTooltipItem <WorkflowActionTooltipItem
id="node-details" id="node-details"
@@ -305,10 +322,7 @@ function VisualizerNode({
> >
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}> <NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
<NodeResourceName id={`node-${node.id}-name`}> <NodeResourceName id={`node-${node.id}-name`}>
{node?.fullUnifiedJobTemplate?.name || {nodeName}
node?.originalNodeObject?.summary_fields?.unified_job_template
?.name ||
t`DELETED`}
</NodeResourceName> </NodeResourceName>
</NodeContents> </NodeContents>
</foreignObject> </foreignObject>

View File

@@ -12,3 +12,8 @@ export const toTitleCase = (string) => {
export const arrayToString = (value) => value.join(','); export const arrayToString = (value) => value.join(',');
export const stringToArray = (value) => value.split(',').filter((val) => !!val); export const stringToArray = (value) => value.split(',').filter((val) => !!val);
export const stringIsUUID = (value) =>
/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test(
value
);