Adds blanket error handling to visualizer save process

This commit is contained in:
mabashian 2020-12-09 15:44:18 -05:00
parent 7a3382dd76
commit ca1e597a4d

View File

@ -1,17 +1,21 @@
import React, { useEffect, useReducer } from 'react';
import React, { useCallback, useEffect, useReducer } from 'react';
import { useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import styled from 'styled-components';
import { shape } from 'prop-types';
import { t } from '@lingui/macro';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '../../../contexts/Workflow';
import { getAddedAndRemoved } from '../../../util/lists';
import AlertModal from '../../../components/AlertModal';
import ErrorDetail from '../../../components/ErrorDetail';
import { layoutGraph } from '../../../components/Workflow/WorkflowUtils';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import workflowReducer from '../../../components/Workflow/workflowReducer';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { DeleteAllNodesModal, UnsavedChangesModal } from './Modals';
import {
LinkAddModal,
@ -246,232 +250,6 @@ function Visualizer({ template, i18n }) {
return disassociateNodeRequests;
};
const generateLinkMapAndNewLinks = originalLinkMap => {
const linkMap = {};
const newLinks = [];
links.forEach(link => {
if (link.source.id !== 1) {
const realLinkSourceId = originalLinkMap[link.source.id].id;
const realLinkTargetId = originalLinkMap[link.target.id].id;
if (!linkMap[realLinkSourceId]) {
linkMap[realLinkSourceId] = {};
}
linkMap[realLinkSourceId][realLinkTargetId] = link.linkType;
switch (link.linkType) {
case 'success':
if (
!originalLinkMap[link.source.id].success_nodes.includes(
originalLinkMap[link.target.id].id
)
) {
newLinks.push(link);
}
break;
case 'failure':
if (
!originalLinkMap[link.source.id].failure_nodes.includes(
originalLinkMap[link.target.id].id
)
) {
newLinks.push(link);
}
break;
case 'always':
if (
!originalLinkMap[link.source.id].always_nodes.includes(
originalLinkMap[link.target.id].id
)
) {
newLinks.push(link);
}
break;
default:
}
}
});
return [linkMap, newLinks];
};
const handleVisualizerSave = async () => {
const nodeRequests = [];
const approvalTemplateRequests = [];
const originalLinkMap = {};
const deletedNodeIds = [];
const associateCredentialRequests = [];
const disassociateCredentialRequests = [];
nodes.forEach(node => {
// node with id=1 is the artificial start node
if (node.id === 1) {
return;
}
if (node.originalNodeObject && !node.isDeleted) {
const {
id,
success_nodes,
failure_nodes,
always_nodes,
} = node.originalNodeObject;
originalLinkMap[node.id] = {
id,
success_nodes,
failure_nodes,
always_nodes,
};
}
if (node.isDeleted && node.originalNodeObject) {
deletedNodeIds.push(node.originalNodeObject.id);
nodeRequests.push(
WorkflowJobTemplateNodesAPI.destroy(node.originalNodeObject.id)
);
} else if (!node.isDeleted && !node.originalNodeObject) {
if (node.fullUnifiedJobTemplate.type === 'workflow_approval_template') {
nodeRequests.push(
WorkflowJobTemplatesAPI.createNode(template.id, {}).then(
({ data }) => {
node.originalNodeObject = data;
originalLinkMap[node.id] = {
id: data.id,
success_nodes: [],
failure_nodes: [],
always_nodes: [],
};
approvalTemplateRequests.push(
WorkflowJobTemplateNodesAPI.createApprovalTemplate(data.id, {
name: node.fullUnifiedJobTemplate.name,
description: node.fullUnifiedJobTemplate.description,
timeout: node.fullUnifiedJobTemplate.timeout,
})
);
}
)
);
} else {
nodeRequests.push(
WorkflowJobTemplatesAPI.createNode(template.id, {
...node.promptValues,
inventory: node.promptValues?.inventory?.id || null,
unified_job_template: node.fullUnifiedJobTemplate.id,
}).then(({ data }) => {
node.originalNodeObject = data;
originalLinkMap[node.id] = {
id: data.id,
success_nodes: [],
failure_nodes: [],
always_nodes: [],
};
if (node.promptValues?.removedCredentials?.length > 0) {
node.promptValues.removedCredentials.forEach(cred => {
disassociateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.disassociateCredentials(
data.id,
cred.id
)
);
});
}
if (node.promptValues?.addedCredentials?.length > 0) {
node.promptValues.addedCredentials.forEach(cred => {
associateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.associateCredentials(
data.id,
cred.id
)
);
});
}
})
);
}
} else if (node.isEdited) {
if (node.fullUnifiedJobTemplate.type === 'workflow_approval_template') {
if (
node.originalNodeObject.summary_fields.unified_job_template
.unified_job_type === 'workflow_approval'
) {
approvalTemplateRequests.push(
WorkflowApprovalTemplatesAPI.update(
node.originalNodeObject.summary_fields.unified_job_template.id,
{
name: node.fullUnifiedJobTemplate.name,
description: node.fullUnifiedJobTemplate.description,
timeout: node.fullUnifiedJobTemplate.timeout,
}
)
);
} else {
approvalTemplateRequests.push(
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
node.originalNodeObject.id,
{
name: node.fullUnifiedJobTemplate.name,
description: node.fullUnifiedJobTemplate.description,
timeout: node.fullUnifiedJobTemplate.timeout,
}
)
);
}
} else {
nodeRequests.push(
WorkflowJobTemplateNodesAPI.replace(node.originalNodeObject.id, {
...node.promptValues,
inventory: node.promptValues?.inventory?.id || null,
unified_job_template: node.fullUnifiedJobTemplate.id,
})
);
const {
added: addedCredentials,
removed: removedCredentials,
} = getAddedAndRemoved(
getAggregatedCredentials(
node?.originalNodeCredentials,
node.launchConfig?.defaults?.credentials
),
node.promptValues?.credentials
);
if (addedCredentials.length > 0) {
addedCredentials.forEach(cred => {
associateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.associateCredentials(
node.originalNodeObject.id,
cred.id
)
);
});
}
if (removedCredentials?.length > 0) {
removedCredentials.forEach(cred =>
disassociateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.disassociateCredentials(
node.originalNodeObject.id,
cred.id
)
)
);
}
}
}
});
await Promise.all(nodeRequests);
// Creating approval templates needs to happen after the node has been created
// since we reference the node in the approval template request.
await Promise.all(approvalTemplateRequests);
const [linkMap, newLinks] = generateLinkMapAndNewLinks(originalLinkMap);
await Promise.all(
disassociateNodes(originalLinkMap, deletedNodeIds, linkMap)
);
await Promise.all(associateNodes(newLinks, originalLinkMap));
await Promise.all(disassociateCredentialRequests);
await Promise.all(associateCredentialRequests);
history.push(`/templates/workflow_job_template/${template.id}/details`);
};
useEffect(() => {
async function fetchData() {
try {
@ -505,6 +283,249 @@ function Visualizer({ template, i18n }) {
}
}, [links, nodes]);
const { error: saveVisualizerError, request: saveVisualizer } = useRequest(
useCallback(async () => {
const nodeRequests = [];
const approvalTemplateRequests = [];
const originalLinkMap = {};
const deletedNodeIds = [];
const associateCredentialRequests = [];
const disassociateCredentialRequests = [];
const generateLinkMapAndNewLinks = () => {
const linkMap = {};
const newLinks = [];
links.forEach(link => {
if (link.source.id !== 1) {
const realLinkSourceId = originalLinkMap[link.source.id].id;
const realLinkTargetId = originalLinkMap[link.target.id].id;
if (!linkMap[realLinkSourceId]) {
linkMap[realLinkSourceId] = {};
}
linkMap[realLinkSourceId][realLinkTargetId] = link.linkType;
switch (link.linkType) {
case 'success':
if (
!originalLinkMap[link.source.id].success_nodes.includes(
originalLinkMap[link.target.id].id
)
) {
newLinks.push(link);
}
break;
case 'failure':
if (
!originalLinkMap[link.source.id].failure_nodes.includes(
originalLinkMap[link.target.id].id
)
) {
newLinks.push(link);
}
break;
case 'always':
if (
!originalLinkMap[link.source.id].always_nodes.includes(
originalLinkMap[link.target.id].id
)
) {
newLinks.push(link);
}
break;
default:
}
}
});
return [linkMap, newLinks];
};
nodes.forEach(node => {
// node with id=1 is the artificial start node
if (node.id === 1) {
return;
}
if (node.originalNodeObject && !node.isDeleted) {
const {
id,
success_nodes,
failure_nodes,
always_nodes,
} = node.originalNodeObject;
originalLinkMap[node.id] = {
id,
success_nodes,
failure_nodes,
always_nodes,
};
}
if (node.isDeleted && node.originalNodeObject) {
deletedNodeIds.push(node.originalNodeObject.id);
nodeRequests.push(
WorkflowJobTemplateNodesAPI.destroy(node.originalNodeObject.id)
);
} else if (!node.isDeleted && !node.originalNodeObject) {
if (
node.fullUnifiedJobTemplate.type === 'workflow_approval_template'
) {
nodeRequests.push(
WorkflowJobTemplatesAPI.createNode(template.id, {}).then(
({ data }) => {
node.originalNodeObject = data;
originalLinkMap[node.id] = {
id: data.id,
success_nodes: [],
failure_nodes: [],
always_nodes: [],
};
approvalTemplateRequests.push(
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
data.id,
{
name: node.fullUnifiedJobTemplate.name,
description: node.fullUnifiedJobTemplate.description,
timeout: node.fullUnifiedJobTemplate.timeout,
}
)
);
}
)
);
} else {
nodeRequests.push(
WorkflowJobTemplatesAPI.createNode(template.id, {
...node.promptValues,
inventory: node.promptValues?.inventory?.id || null,
unified_job_template: node.fullUnifiedJobTemplate.id,
}).then(({ data }) => {
node.originalNodeObject = data;
originalLinkMap[node.id] = {
id: data.id,
success_nodes: [],
failure_nodes: [],
always_nodes: [],
};
if (node.promptValues?.removedCredentials?.length > 0) {
node.promptValues.removedCredentials.forEach(cred => {
disassociateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.disassociateCredentials(
data.id,
cred.id
)
);
});
}
if (node.promptValues?.addedCredentials?.length > 0) {
node.promptValues.addedCredentials.forEach(cred => {
associateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.associateCredentials(
data.id,
cred.id
)
);
});
}
})
);
}
} else if (node.isEdited) {
if (
node.fullUnifiedJobTemplate.type === 'workflow_approval_template'
) {
if (
node.originalNodeObject.summary_fields.unified_job_template
.unified_job_type === 'workflow_approval'
) {
approvalTemplateRequests.push(
WorkflowApprovalTemplatesAPI.update(
node.originalNodeObject.summary_fields.unified_job_template
.id,
{
name: node.fullUnifiedJobTemplate.name,
description: node.fullUnifiedJobTemplate.description,
timeout: node.fullUnifiedJobTemplate.timeout,
}
)
);
} else {
approvalTemplateRequests.push(
WorkflowJobTemplateNodesAPI.createApprovalTemplate(
node.originalNodeObject.id,
{
name: node.fullUnifiedJobTemplate.name,
description: node.fullUnifiedJobTemplate.description,
timeout: node.fullUnifiedJobTemplate.timeout,
}
)
);
}
} else {
nodeRequests.push(
WorkflowJobTemplateNodesAPI.replace(node.originalNodeObject.id, {
...node.promptValues,
inventory: node.promptValues?.inventory?.id || null,
unified_job_template: node.fullUnifiedJobTemplate.id,
}).then(() => {
const {
added: addedCredentials,
removed: removedCredentials,
} = getAddedAndRemoved(
getAggregatedCredentials(
node?.originalNodeCredentials,
node.launchConfig?.defaults?.credentials
),
node.promptValues?.credentials
);
if (addedCredentials.length > 0) {
addedCredentials.forEach(cred => {
associateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.associateCredentials(
node.originalNodeObject.id,
cred.id
)
);
});
}
if (removedCredentials?.length > 0) {
removedCredentials.forEach(cred =>
disassociateCredentialRequests.push(
WorkflowJobTemplateNodesAPI.disassociateCredentials(
node.originalNodeObject.id,
cred.id
)
)
);
}
})
);
}
}
});
await Promise.all(nodeRequests);
// Creating approval templates needs to happen after the node has been created
// since we reference the node in the approval template request.
await Promise.all(approvalTemplateRequests);
const [linkMap, newLinks] = generateLinkMapAndNewLinks(originalLinkMap);
await Promise.all(
disassociateNodes(originalLinkMap, deletedNodeIds, linkMap)
);
await Promise.all(associateNodes(newLinks, originalLinkMap));
await Promise.all(disassociateCredentialRequests);
await Promise.all(associateCredentialRequests);
history.push(`/templates/workflow_job_template/${template.id}/details`);
}, [links, nodes, history, template.id]),
{}
);
const {
error: nodeRequestError,
dismissError: dismissNodeRequestError,
} = useDismissableError(saveVisualizerError);
if (isLoading) {
return (
<CenteredContent>
@ -529,7 +550,7 @@ function Visualizer({ template, i18n }) {
<Wrapper>
<VisualizerToolbar
onClose={handleVisualizerClose}
onSave={handleVisualizerSave}
onSave={() => saveVisualizer(nodes)}
hasUnsavedChanges={unsavedChanges}
template={template}
readOnly={readOnly}
@ -553,11 +574,22 @@ function Visualizer({ template, i18n }) {
`/templates/workflow_job_template/${template.id}/details`
)
}
onSaveAndExit={() => handleVisualizerSave()}
onSaveAndExit={() => saveVisualizer(nodes)}
/>
)}
{showDeleteAllNodesModal && <DeleteAllNodesModal />}
{nodeToView && <NodeViewModal readOnly={readOnly} />}
{nodeRequestError && (
<AlertModal
isOpen
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissNodeRequestError}
>
{i18n._(t`There was an error saving the workflow.`)}
<ErrorDetail error={nodeRequestError} />
</AlertModal>
)}
</WorkflowDispatchContext.Provider>
</WorkflowStateContext.Provider>
);