mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge pull request #10592 from mabashian/2032-workflow-node-alias
Adds support for workflow node aliasing via identifier field
This commit is contained in:
commit
dae3f1a164
@ -6,6 +6,7 @@ import styled from 'styled-components';
|
||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||
import { shape } from 'prop-types';
|
||||
import { secondsToHHMMSS } from 'util/dates';
|
||||
import { stringIsUUID } from 'util/strings';
|
||||
|
||||
const GridDL = styled.dl`
|
||||
column-gap: 15px;
|
||||
@ -37,6 +38,17 @@ function WorkflowNodeHelp({ node }) {
|
||||
const unifiedJobTemplate =
|
||||
node?.fullUnifiedJobTemplate ||
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template;
|
||||
let identifier = null;
|
||||
if (node?.identifier) {
|
||||
({ identifier } = node);
|
||||
} else if (
|
||||
node?.originalNodeObject?.identifier &&
|
||||
!stringIsUUID(node.originalNodeObject.identifier)
|
||||
) {
|
||||
({
|
||||
originalNodeObject: { identifier },
|
||||
} = node);
|
||||
}
|
||||
if (unifiedJobTemplate || job) {
|
||||
const type = unifiedJobTemplate
|
||||
? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type
|
||||
@ -132,10 +144,18 @@ function WorkflowNodeHelp({ node }) {
|
||||
)}
|
||||
{job && (
|
||||
<GridDL>
|
||||
{identifier && (
|
||||
<>
|
||||
<dt>
|
||||
<b>{t`Node Alias`}</b>
|
||||
</dt>
|
||||
<dd id="workflow-node-help-alias">{identifier}</dd>
|
||||
</>
|
||||
)}
|
||||
<dt>
|
||||
<b>{t`Name`}</b>
|
||||
<b>{t`Resource Name`}</b>
|
||||
</dt>
|
||||
<dd id="workflow-node-help-name">{job.name}</dd>
|
||||
<dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd>
|
||||
<dt>
|
||||
<b>{t`Type`}</b>
|
||||
</dt>
|
||||
@ -158,8 +178,16 @@ function WorkflowNodeHelp({ node }) {
|
||||
)}
|
||||
{unifiedJobTemplate && !job && (
|
||||
<GridDL>
|
||||
{identifier && (
|
||||
<>
|
||||
<dt>
|
||||
<b>{t`Node Alias`}</b>
|
||||
</dt>
|
||||
<dd id="workflow-node-help-alias">{identifier}</dd>
|
||||
</>
|
||||
)}
|
||||
<dt>
|
||||
<b>{t`Name`}</b>
|
||||
<b>{t`Resource Name`}</b>
|
||||
</dt>
|
||||
<dd id="workflow-node-help-name">{unifiedJobTemplate.name}</dd>
|
||||
<dt>
|
||||
|
||||
@ -2,29 +2,32 @@ import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import WorkflowNodeHelp from './WorkflowNodeHelp';
|
||||
|
||||
describe('WorkflowNodeHelp', () => {
|
||||
test('successfully mounts', () => {
|
||||
const wrapper = mountWithContexts(<WorkflowNodeHelp node={{}} />);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
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: {
|
||||
const node = {
|
||||
originalNodeObject: {
|
||||
identifier: 'Foo',
|
||||
summary_fields: {
|
||||
job: {
|
||||
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} />);
|
||||
expect(wrapper.find('#workflow-node-help-alias').text()).toBe('Foo');
|
||||
expect(wrapper.find('#workflow-node-help-name').text()).toBe(
|
||||
'Foo Job Template'
|
||||
);
|
||||
|
||||
@ -184,6 +184,7 @@ function createNode(state, node) {
|
||||
isInvalidLinkTarget: false,
|
||||
promptValues: node.promptValues,
|
||||
all_parents_must_converge: node.all_parents_must_converge,
|
||||
identifier: node.identifier,
|
||||
});
|
||||
|
||||
// Ensures that root nodes appear to always run
|
||||
@ -660,17 +661,16 @@ function updateNode(state, editedNode) {
|
||||
launchConfig,
|
||||
promptValues,
|
||||
all_parents_must_converge,
|
||||
identifier,
|
||||
} = editedNode;
|
||||
const newNodes = [...nodes];
|
||||
|
||||
const matchingNode = newNodes.find((node) => node.id === nodeToEdit.id);
|
||||
matchingNode.all_parents_must_converge = all_parents_must_converge;
|
||||
if (matchingNode.originalNodeObject) {
|
||||
delete matchingNode.originalNodeObject.all_parents_must_converge;
|
||||
}
|
||||
matchingNode.fullUnifiedJobTemplate = nodeResource;
|
||||
matchingNode.isEdited = true;
|
||||
matchingNode.launchConfig = launchConfig;
|
||||
matchingNode.identifier = identifier;
|
||||
|
||||
if (promptValues) {
|
||||
matchingNode.promptValues = promptValues;
|
||||
|
||||
@ -65,6 +65,7 @@ const workflowContext = {
|
||||
{
|
||||
id: 2,
|
||||
originalNodeObject: {
|
||||
identifier: 'Node identifier',
|
||||
summary_fields: {
|
||||
job: {
|
||||
name: 'Foo JT',
|
||||
@ -72,6 +73,10 @@ const workflowContext = {
|
||||
status: 'successful',
|
||||
elapsed: 60,
|
||||
},
|
||||
unified_job_template: {
|
||||
name: 'Foo JT',
|
||||
type: 'job_template',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -185,9 +190,17 @@ describe('WorkflowOutputGraph', () => {
|
||||
expect(wrapper.find('WorkflowLinkHelp')).toHaveLength(0);
|
||||
wrapper.find('g#node-2').simulate('mouseenter');
|
||||
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1);
|
||||
expect(wrapper.find('WorkflowNodeHelp').contains(<b>Name</b>)).toEqual(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
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(
|
||||
wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>)
|
||||
).toEqual(true);
|
||||
|
||||
@ -8,6 +8,7 @@ import { WorkflowStateContext } from 'contexts/Workflow';
|
||||
import StatusIcon from 'components/StatusIcon';
|
||||
import { WorkflowNodeTypeLetter } from 'components/Workflow';
|
||||
import { secondsToHHMMSS } from 'util/dates';
|
||||
import { stringIsUUID } from 'util/strings';
|
||||
import { constants as wfConstants } from 'components/Workflow/WorkflowUtils';
|
||||
|
||||
const NodeG = styled.g`
|
||||
@ -67,9 +68,6 @@ function WorkflowOutputNode({ mouseEnter, mouseLeave, node }) {
|
||||
const history = useHistory();
|
||||
const { nodePositions } = useContext(WorkflowStateContext);
|
||||
const job = node?.originalNodeObject?.summary_fields?.job;
|
||||
const jobName =
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template?.name ||
|
||||
node?.unifiedJobTemplate?.name;
|
||||
|
||||
let borderColor = '#93969A';
|
||||
|
||||
@ -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 (
|
||||
<NodeG
|
||||
id={`node-${node.id}`}
|
||||
@ -144,14 +159,14 @@ function WorkflowOutputNode({ mouseEnter, mouseLeave, node }) {
|
||||
<>
|
||||
<JobTopLine>
|
||||
{job.status !== 'pending' && <StatusIcon status={job.status} />}
|
||||
<p>{jobName}</p>
|
||||
<p>{nodeName}</p>
|
||||
</JobTopLine>
|
||||
{!!job?.elapsed && (
|
||||
<Elapsed>{secondsToHHMMSS(job.elapsed)}</Elapsed>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<NodeDefaultLabel>{jobName || t`DELETED`}</NodeDefaultLabel>
|
||||
<NodeDefaultLabel>{nodeName}</NodeDefaultLabel>
|
||||
)}
|
||||
</NodeContents>
|
||||
</foreignObject>
|
||||
|
||||
@ -102,7 +102,6 @@ function updateNode(nodes, index, message) {
|
||||
job: {
|
||||
...nodes[index]?.job,
|
||||
id: message.unified_job_id,
|
||||
name: nodes[index]?.job?.name || nodes[index]?.unifiedJobTemplate?.name,
|
||||
status: message.status,
|
||||
type: message.type,
|
||||
},
|
||||
|
||||
@ -20,6 +20,7 @@ function NodeAddModal() {
|
||||
timeoutSeconds,
|
||||
linkType,
|
||||
convergence,
|
||||
identifier,
|
||||
} = values;
|
||||
|
||||
if (values) {
|
||||
@ -35,6 +36,7 @@ function NodeAddModal() {
|
||||
const node = {
|
||||
linkType,
|
||||
all_parents_must_converge: convergence === 'all',
|
||||
identifier,
|
||||
};
|
||||
|
||||
delete values.convergence;
|
||||
|
||||
@ -18,6 +18,7 @@ function NodeEditModal() {
|
||||
timeoutMinutes,
|
||||
timeoutSeconds,
|
||||
convergence,
|
||||
identifier,
|
||||
...rest
|
||||
} = values;
|
||||
let node;
|
||||
@ -30,11 +31,13 @@ function NodeEditModal() {
|
||||
timeout: Number(timeoutMinutes) * 60 + Number(timeoutSeconds),
|
||||
type: 'workflow_approval_template',
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
} else {
|
||||
node = {
|
||||
nodeResource,
|
||||
all_parents_must_converge: convergence === 'all',
|
||||
identifier,
|
||||
};
|
||||
if (nodeType === 'job_template' || nodeType === 'workflow_job_template') {
|
||||
node.promptValues = {
|
||||
|
||||
@ -536,6 +536,7 @@ describe('Edit existing node', () => {
|
||||
value={{
|
||||
nodeToEdit: {
|
||||
id: 2,
|
||||
identifier: 'Foo',
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Test Project',
|
||||
@ -602,6 +603,7 @@ describe('Edit existing node', () => {
|
||||
expect(onSave).toBeCalledWith(
|
||||
{
|
||||
convergence: 'any',
|
||||
identifier: 'Foo',
|
||||
approvalDescription: 'Test Approval Description',
|
||||
approvalName: 'Test Approval',
|
||||
linkType: 'success',
|
||||
@ -622,6 +624,7 @@ describe('Edit existing node', () => {
|
||||
value={{
|
||||
nodeToEdit: {
|
||||
id: 2,
|
||||
identifier: 'Foo',
|
||||
fullUnifiedJobTemplate: {
|
||||
id: 1,
|
||||
name: 'Test Approval',
|
||||
@ -672,6 +675,7 @@ describe('Edit existing node', () => {
|
||||
expect(onSave).toBeCalledWith(
|
||||
{
|
||||
convergence: 'any',
|
||||
identifier: 'Foo',
|
||||
linkType: 'success',
|
||||
nodeResource: {
|
||||
id: 1,
|
||||
|
||||
@ -31,7 +31,7 @@ const NodeTypeErrorAlert = styled(Alert)`
|
||||
`;
|
||||
|
||||
const TimeoutInput = styled(TextInput)`
|
||||
width: 200px;
|
||||
width: 200px !important;
|
||||
:not(:first-of-type) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
@ -43,7 +43,7 @@ const TimeoutLabel = styled.p`
|
||||
min-width: fit-content;
|
||||
`;
|
||||
|
||||
function NodeTypeStep() {
|
||||
function NodeTypeStep({ isIdentifierRequired }) {
|
||||
const [nodeTypeField, , nodeTypeHelpers] = useField('nodeType');
|
||||
const [nodeResourceField, nodeResourceMeta, nodeResourceHelpers] =
|
||||
useField('nodeResource');
|
||||
@ -157,105 +157,117 @@ function NodeTypeStep() {
|
||||
)}
|
||||
<Form css="margin-top: 20px;">
|
||||
<FormColumnLayout>
|
||||
{nodeTypeField.value === 'workflow_approval_template' && (
|
||||
<FormFullWidthLayout>
|
||||
<FormField
|
||||
name="approvalName"
|
||||
id="approval-name"
|
||||
isRequired
|
||||
validate={required(null)}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
label={t`Name`}
|
||||
/>
|
||||
<FormField
|
||||
name="approvalDescription"
|
||||
id="approval-description"
|
||||
label={t`Description`}
|
||||
/>
|
||||
<FormGroup
|
||||
label={t`Timeout`}
|
||||
fieldId="approval-timeout"
|
||||
name="timeout"
|
||||
>
|
||||
<div css="display: flex;align-items: center;">
|
||||
<TimeoutInput
|
||||
{...timeoutMinutesField}
|
||||
aria-label={t`Timeout minutes`}
|
||||
id="approval-timeout-minutes"
|
||||
min="0"
|
||||
onChange={(value, event) => {
|
||||
timeoutMinutesField.onChange(event);
|
||||
}}
|
||||
step="1"
|
||||
type="number"
|
||||
/>
|
||||
<TimeoutLabel>
|
||||
<Trans>min</Trans>
|
||||
</TimeoutLabel>
|
||||
<TimeoutInput
|
||||
{...timeoutSecondsField}
|
||||
aria-label={t`Timeout seconds`}
|
||||
id="approval-timeout-seconds"
|
||||
min="0"
|
||||
onChange={(value, event) => {
|
||||
timeoutSecondsField.onChange(event);
|
||||
}}
|
||||
step="1"
|
||||
type="number"
|
||||
/>
|
||||
<TimeoutLabel>
|
||||
<Trans>sec</Trans>
|
||||
</TimeoutLabel>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</FormFullWidthLayout>
|
||||
)}
|
||||
<FormGroup
|
||||
fieldId="convergence"
|
||||
label={t`Convergence`}
|
||||
isRequired
|
||||
labelIcon={
|
||||
<Popover
|
||||
content={
|
||||
<>
|
||||
{t`Preconditions for running this node when there are multiple parents. Refer to the`}{' '}
|
||||
<a
|
||||
href={`${getDocsBaseUrl(
|
||||
config
|
||||
)}/html/userguide/workflow_templates.html#convergence-node`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t`documentation`}
|
||||
</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"
|
||||
<FormFullWidthLayout>
|
||||
{nodeTypeField.value === 'workflow_approval_template' && (
|
||||
<>
|
||||
<FormField
|
||||
name="approvalName"
|
||||
id="approval-name"
|
||||
isRequired
|
||||
validate={required(null)}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
label={t`Name`}
|
||||
/>
|
||||
<FormField
|
||||
name="approvalDescription"
|
||||
id="approval-description"
|
||||
label={t`Description`}
|
||||
/>
|
||||
<FormGroup
|
||||
label={t`Timeout`}
|
||||
fieldId="approval-timeout"
|
||||
name="timeout"
|
||||
>
|
||||
<div css="display: flex;align-items: center;">
|
||||
<TimeoutInput
|
||||
{...timeoutMinutesField}
|
||||
aria-label={t`Timeout minutes`}
|
||||
id="approval-timeout-minutes"
|
||||
min="0"
|
||||
onChange={(value, event) => {
|
||||
timeoutMinutesField.onChange(event);
|
||||
}}
|
||||
step="1"
|
||||
type="number"
|
||||
/>
|
||||
<TimeoutLabel>
|
||||
<Trans>min</Trans>
|
||||
</TimeoutLabel>
|
||||
<TimeoutInput
|
||||
{...timeoutSecondsField}
|
||||
aria-label={t`Timeout seconds`}
|
||||
id="approval-timeout-seconds"
|
||||
min="0"
|
||||
onChange={(value, event) => {
|
||||
timeoutSecondsField.onChange(event);
|
||||
}}
|
||||
step="1"
|
||||
type="number"
|
||||
/>
|
||||
<TimeoutLabel>
|
||||
<Trans>sec</Trans>
|
||||
</TimeoutLabel>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<FormGroup
|
||||
fieldId="convergence"
|
||||
label={t`Convergence`}
|
||||
isRequired
|
||||
labelIcon={
|
||||
<Popover
|
||||
content={
|
||||
<>
|
||||
{t`Preconditions for running this node when there are multiple parents. Refer to the`}{' '}
|
||||
<a
|
||||
href={`${getDocsBaseUrl(
|
||||
config
|
||||
)}/html/userguide/workflow_templates.html#convergence-node`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t`documentation`}
|
||||
</a>{' '}
|
||||
{t`for more info.`}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
{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>
|
||||
</Form>
|
||||
</>
|
||||
|
||||
@ -2,14 +2,16 @@ import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import StepName from 'components/LaunchPrompt/steps/StepName';
|
||||
import { stringIsUUID } from 'util/strings';
|
||||
import NodeTypeStep from './NodeTypeStep';
|
||||
|
||||
const STEP_ID = 'nodeType';
|
||||
|
||||
export default function useNodeTypeStep() {
|
||||
export default function useNodeTypeStep(nodeToEdit) {
|
||||
const [, meta] = useField('nodeType');
|
||||
const [approvalNameField] = useField('approvalName');
|
||||
const [nodeTypeField, ,] = useField('nodeType');
|
||||
const [, identifierMeta] = useField('identifier');
|
||||
const [nodeResourceField, nodeResourceMeta] = useField({
|
||||
name: 'nodeResource',
|
||||
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 {
|
||||
step: getStep(
|
||||
nodeTypeField,
|
||||
approvalNameField,
|
||||
nodeResourceField,
|
||||
formError
|
||||
formError,
|
||||
nodeToEdit
|
||||
),
|
||||
initialValues: getInitialValues(),
|
||||
isReady: true,
|
||||
@ -49,7 +53,8 @@ function getStep(
|
||||
nodeTypeField,
|
||||
approvalNameField,
|
||||
nodeResourceField,
|
||||
formError
|
||||
formError,
|
||||
nodeToEdit
|
||||
) {
|
||||
const isEnabled = () => {
|
||||
if (
|
||||
@ -70,7 +75,15 @@ function getStep(
|
||||
{t`Node type`}
|
||||
</StepName>
|
||||
),
|
||||
component: <NodeTypeStep />,
|
||||
component: (
|
||||
<NodeTypeStep
|
||||
isIdentifierRequired={
|
||||
nodeToEdit &&
|
||||
nodeToEdit.originalNodeObject &&
|
||||
!stringIsUUID(nodeToEdit.originalNodeObject?.identifier)
|
||||
}
|
||||
/>
|
||||
),
|
||||
enableNext: isEnabled(),
|
||||
};
|
||||
}
|
||||
@ -83,5 +96,6 @@ function getInitialValues() {
|
||||
timeoutSeconds: 0,
|
||||
nodeType: 'job_template',
|
||||
convergence: 'any',
|
||||
identifier: '',
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ 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 { stringIsUUID } from 'util/strings';
|
||||
import useNodeTypeStep from './NodeTypeStep/useNodeTypeStep';
|
||||
import useDaysToKeepStep from './useDaysToKeepStep';
|
||||
import useRunTypeStep from './useRunTypeStep';
|
||||
@ -37,6 +38,22 @@ const getNodeToEditDefaultValues = (
|
||||
nodeToEdit,
|
||||
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 = {
|
||||
nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null,
|
||||
nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template',
|
||||
@ -45,6 +62,7 @@ const getNodeToEditDefaultValues = (
|
||||
nodeToEdit?.originalNodeObject?.all_parents_must_converge
|
||||
? 'all'
|
||||
: 'any',
|
||||
identifier,
|
||||
};
|
||||
|
||||
if (
|
||||
@ -274,6 +292,8 @@ export default function useWorkflowNodeSteps(
|
||||
}),
|
||||
{}
|
||||
);
|
||||
initialValues.identifier = formikValues.identifier;
|
||||
initialValues.convergence = formikValues.convergence;
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
if (initialValues.convergence === 'all') {
|
||||
formikValues.convergence = 'all';
|
||||
}
|
||||
|
||||
resetForm({
|
||||
errors,
|
||||
values: {
|
||||
...initialValues,
|
||||
convergence: formikValues.convergence,
|
||||
nodeResource: formikValues.nodeResource,
|
||||
nodeType: formikValues.nodeType,
|
||||
linkType: formikValues.linkType,
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
WorkflowStateContext,
|
||||
} from 'contexts/Workflow';
|
||||
import { getAddedAndRemoved } from 'util/lists';
|
||||
import { stringIsUUID } from 'util/strings';
|
||||
import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import { layoutGraph } from 'components/Workflow/WorkflowUtils';
|
||||
@ -51,6 +52,20 @@ const Wrapper = styled.div`
|
||||
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 = (
|
||||
originalNodeOverride = [],
|
||||
templateDefaultCredentials = []
|
||||
@ -366,6 +381,7 @@ function Visualizer({ template }) {
|
||||
nodeRequests.push(
|
||||
WorkflowJobTemplatesAPI.createNode(template.id, {
|
||||
all_parents_must_converge: node.all_parents_must_converge,
|
||||
...(node.identifier && { identifier: node.identifier }),
|
||||
}).then(({ data }) => {
|
||||
node.originalNodeObject = data;
|
||||
originalLinkMap[node.id] = {
|
||||
@ -390,6 +406,7 @@ function Visualizer({ template }) {
|
||||
inventory: node.promptValues?.inventory?.id || null,
|
||||
unified_job_template: node.fullUnifiedJobTemplate.id,
|
||||
all_parents_must_converge: node.all_parents_must_converge,
|
||||
...(node.identifier && { identifier: node.identifier }),
|
||||
}).then(({ data }) => {
|
||||
node.originalNodeObject = data;
|
||||
originalLinkMap[node.id] = {
|
||||
@ -425,6 +442,9 @@ function Visualizer({ template }) {
|
||||
node.originalNodeObject.id,
|
||||
{
|
||||
all_parents_must_converge: node.all_parents_must_converge,
|
||||
...(replaceIdentifier(node) && {
|
||||
identifier: node.identifier,
|
||||
}),
|
||||
}
|
||||
).then(({ data }) => {
|
||||
node.originalNodeObject = data;
|
||||
@ -447,6 +467,9 @@ function Visualizer({ template }) {
|
||||
node.originalNodeObject.id,
|
||||
{
|
||||
all_parents_must_converge: node.all_parents_must_converge,
|
||||
...(replaceIdentifier(node) && {
|
||||
identifier: node.identifier,
|
||||
}),
|
||||
}
|
||||
).then(({ data }) => {
|
||||
node.originalNodeObject = data;
|
||||
@ -470,6 +493,9 @@ function Visualizer({ template }) {
|
||||
inventory: node.promptValues?.inventory?.id || null,
|
||||
unified_job_template: node.fullUnifiedJobTemplate.id,
|
||||
all_parents_must_converge: node.all_parents_must_converge,
|
||||
...(replaceIdentifier(node) && {
|
||||
identifier: node.identifier,
|
||||
}),
|
||||
}).then(() => {
|
||||
const { added: addedCredentials, removed: removedCredentials } =
|
||||
getAddedAndRemoved(
|
||||
|
||||
@ -68,15 +68,19 @@ const workflowContext = {
|
||||
name: 'Foo JT',
|
||||
type: 'job_template',
|
||||
},
|
||||
identifier: 'node 2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
identifier: 'node 3',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
identifier: 'node 4',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
identifier: 'node 5',
|
||||
},
|
||||
],
|
||||
showLegend: false,
|
||||
@ -183,9 +187,15 @@ describe('VisualizerGraph', () => {
|
||||
.first()
|
||||
.simulate('mouseenter');
|
||||
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1);
|
||||
expect(wrapper.find('WorkflowNodeHelp').contains(<b>Name</b>)).toEqual(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
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(
|
||||
wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>)
|
||||
).toEqual(true);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useContext, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import {
|
||||
@ -18,6 +17,7 @@ import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import { WorkflowJobTemplateNodesAPI } from 'api';
|
||||
import { constants as wfConstants } from 'components/Workflow/WorkflowUtils';
|
||||
import { stringIsUUID } from 'util/strings';
|
||||
import {
|
||||
WorkflowActionTooltip,
|
||||
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 = (
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-details"
|
||||
@ -305,10 +322,7 @@ function VisualizerNode({
|
||||
>
|
||||
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
||||
<NodeResourceName id={`node-${node.id}-name`}>
|
||||
{node?.fullUnifiedJobTemplate?.name ||
|
||||
node?.originalNodeObject?.summary_fields?.unified_job_template
|
||||
?.name ||
|
||||
t`DELETED`}
|
||||
{nodeName}
|
||||
</NodeResourceName>
|
||||
</NodeContents>
|
||||
</foreignObject>
|
||||
|
||||
@ -12,3 +12,8 @@ export const toTitleCase = (string) => {
|
||||
export const arrayToString = (value) => value.join(',');
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user