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 { shape } from 'prop-types';
import { secondsToHHMMSS } from 'util/dates';
import { stringIsUUID } from 'util/strings';
const GridDL = styled.dl`
column-gap: 15px;
@ -37,6 +38,11 @@ function WorkflowNodeHelp({ node }) {
const unifiedJobTemplate =
node?.fullUnifiedJobTemplate ||
node?.originalNodeObject?.summary_fields?.unified_job_template;
const identifier =
node?.identifier ||
(!stringIsUUID(node?.originalNodeObject?.identifier)
? node.originalNodeObject.identifier
: null);
if (unifiedJobTemplate || job) {
const type = unifiedJobTemplate
? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type
@ -132,10 +138,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 +172,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>

View File

@ -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'
);

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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,
},

View File

@ -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;

View File

@ -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 = {

View File

@ -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,

View File

@ -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>
</>

View File

@ -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: '',
};
}

View File

@ -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,

View File

@ -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(

View File

@ -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);

View File

@ -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>

View File

@ -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
);