mirror of
https://github.com/ansible/awx.git
synced 2026-03-05 18:51:06 -03:30
Adds support for workflow node aliasing via identifier field
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user