From ca1e597a4dec9f9ef70dbe7a198bca4601b0cd8d Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 9 Dec 2020 15:44:18 -0500 Subject: [PATCH] Adds blanket error handling to visualizer save process --- .../Visualizer.jsx | 490 ++++++++++-------- 1 file changed, 261 insertions(+), 229 deletions(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx index 0a79f31edc..1deb449890 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx @@ -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 ( @@ -529,7 +550,7 @@ function Visualizer({ template, i18n }) { 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 && } {nodeToView && } + {nodeRequestError && ( + + {i18n._(t`There was an error saving the workflow.`)} + + + )} );