1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

Fixed a number of workflow visualizer bugs. Added loading spinners while data is being loaded/processed.

This commit is contained in:
mabashian 2018-11-14 16:48:32 -05:00
parent 266831e26d
commit 4a6a3b27fa
8 changed files with 397 additions and 268 deletions

View File

@ -128,7 +128,9 @@ function TemplatesStrings (BaseString) {
EDIT_LINK: t.s('EDIT LINK'),
VIEW_LINK: t.s('VIEW LINK'),
NEW_LINK: t.s('Please click on an available node to form a new link.'),
UNLINK: t.s('UNLINK')
UNLINK: t.s('UNLINK'),
READ_ONLY_PROMPT_VALUES: t.s('The following promptable values were provided when this node was created:'),
READ_ONLY_NO_PROMPT_VALUES: t.s('No promptable values were provided when this node was created.')
};
}

View File

@ -114,8 +114,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
// There's something off with the math on the root node...
if (d.source.id === 1) {
if (scope.mode === "details") {
sourceY = sourceY + 17;
} else {
sourceY = sourceY + 10;
}
}
let points = [{
x: sourceX,
@ -1279,7 +1283,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function node_click() {
this.on("click", function(d) {
if(d.id !== scope.graphState.nodeBeingAdded && !scope.readOnly){
if(d.id !== scope.graphState.nodeBeingAdded){
if(scope.graphState.isLinkMode && !d.isInvalidLinkTarget && scope.graphState.addLinkSource !== d.id) {
$('.WorkflowChart-potentialLink').remove();
scope.selectNodeForLinking({

View File

@ -1,42 +1,5 @@
export default [function(){
return {
generateDepthMap: (arrayOfLinks) => {
let depthMap = {};
let nodesWithChildren = {};
let walkBranch = (nodeId, depth) => {
depthMap[nodeId] = depthMap[nodeId] ? (depth > depthMap[nodeId] ? depth : depthMap[nodeId]) : depth;
if (nodesWithChildren[nodeId]) {
_.forEach(nodesWithChildren[nodeId].children, (childNodeId) => {
walkBranch(childNodeId, depth+1);
});
}
};
let rootNodeIds = [];
arrayOfLinks.forEach(link => {
// link.source.index of 0 is our artificial start node
if (link.source.index !== 0) {
if (!nodesWithChildren[link.source.id]) {
nodesWithChildren[link.source.id] = {
children: []
};
}
nodesWithChildren[link.source.id].children.push(link.target.id);
} else {
// Store the fact that might be a root node
rootNodeIds.push(link.target.id);
}
});
_.forEach(rootNodeIds, function(rootNodeId) {
walkBranch(rootNodeId, 1);
depthMap[rootNodeId] = 1;
});
return depthMap;
},
generateArraysOfNodesAndLinks: function(allNodes) {
let nonRootNodeIds = [];
let allNodeIds = [];
@ -77,7 +40,7 @@ export default [function(){
nodeObj.job = node.summary_fields.job;
}
if(node.summary_fields.unified_job_template) {
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
nodeRef[nodeIdCounter].unifiedJobTemplate = nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
}
arrayOfNodesForChart.push(nodeObj);

View File

@ -7,11 +7,11 @@
export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService', 'Rest', '$q',
'TemplatesStrings', 'CreateSelect2', 'Empty', 'generateList', 'QuerySet',
'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList', 'ProcessErrors',
'i18n',
'i18n', 'ParseTypeChange',
function($scope, TemplatesService, JobTemplate, PromptService, Rest, $q,
TemplatesStrings, CreateSelect2, Empty, generateList, qs,
GetBasePath, TemplateList, ProjectList, InventorySourcesList, ProcessErrors,
i18n
i18n, ParseTypeChange
) {
let promptWatcher, credentialsWatcher, surveyQuestionWatcher, listPromises = [];
@ -139,6 +139,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
const finishConfiguringEdit = () => {
if (!$scope.readOnly) {
let jobTemplate = new JobTemplate();
if (_.get($scope, 'nodeConfig.node.promptData') && !_.isEmpty($scope.nodeConfig.node.promptData)) {
@ -363,6 +364,30 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
} else {
$scope.activeTab = "jobs";
}
} else {
$scope.jobTags = $scope.nodeConfig.node.originalNodeObject.job_tags ? $scope.nodeConfig.node.originalNodeObject.job_tags.split(',').map((tag) => (tag)) : [];
$scope.skipTags = $scope.nodeConfig.node.originalNodeObject.skip_tags ? $scope.nodeConfig.node.originalNodeObject.skip_tags.split(',').map((tag) => (tag)) : [];
$scope.showJobTags = true;
$scope.showSkipTags = true;
if (!$.isEmptyObject($scope.nodeConfig.node.originalNodeObject.extra_data)) {
$scope.extraVars = '---\n' + jsyaml.safeDump($scope.nodeConfig.node.originalNodeObject.extra_data);
$scope.showExtraVars = true;
$scope.parseType = 'yaml';
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
field_id: 'workflow_node_form_extra_vars',
readOnly: true
});
} else {
$scope.extraVars = null;
$scope.showExtraVars = false;
}
$scope.nodeFormDataLoaded = true;
}
};

View File

@ -1,11 +1,11 @@
<div ng-show="nodeFormDataLoaded">
<div class="WorkflowMaker-formTitle ng-binding">{{nodeConfig.mode === 'edit' ? node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}</div>
<div class="Form-tabHolder">
<div class="WorkflowMaker-formTitle">{{nodeConfig.mode === 'edit' ? nodeConfig.node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}</div>
<div class="Form-tabHolder" ng-if="!readOnly">
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'jobs'}" ng-click="activeTab = 'jobs'">{{strings.get('workflow_maker.JOBS')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'project_syncs'}" ng-click="activeTab = 'project_syncs'">{{strings.get('workflow_maker.PROJECT_SYNC')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'inventory_syncs'}" ng-click="activeTab = 'inventory_syncs'">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</div>
</div>
<div class="WorkflowMaker-formLists">
<div class="WorkflowMaker-formLists" ng-if="!readOnly">
<div id="workflow-jobs-list" ng-show="activeTab === 'jobs'">
<div ng-hide="wf_maker_templates.length === 0 && (searchTags | isEmpty)">
<smart-search django-model="unified_job_templates" base-path="unified_job_templates" iterator="wf_maker_template" dataset="wf_maker_template_dataset" list="templateList" collection="wf_maker_templates" default-params="wf_maker_template_default_params" query-set="wf_maker_template_queryset" search-bar-full-width="true" search-tags="searchTags">
@ -141,6 +141,98 @@
</select>
</div>
</div>
<div ng-if="readOnly">
<div
class="WorkflowMaker-readOnlyPromptText"
ng-show="nodeConfig.node.originalNodeObject.job_type !== null ||
(promptData.prompts.credentials.value && promptData.prompts.credentials.value.length > 0) ||
nodeConfig.node.originalNodeObject.inventory !== null ||
nodeConfig.node.originalNodeObject.limit !== null ||
nodeConfig.node.originalNodeObject.verbosity !== null ||
nodeConfig.node.originalNodeObject.job_tags !== null ||
nodeConfig.node.originalNodeObject.skip_tags !== null ||
nodeConfig.node.originalNodeObject.diff_mode !== null ||
showExtraVars">
{{:: strings.get('workflow_maker.READ_ONLY_PROMPT_VALUES')}}
</div>
<div
class="WorkflowMaker-readOnlyPromptText"
ng-show="!(nodeConfig.node.originalNodeObject.job_type !== null ||
(promptData.prompts.credentials.value && promptData.prompts.credentials.value.length > 0) ||
nodeConfig.node.originalNodeObject.inventory !== null ||
nodeConfig.node.originalNodeObject.limit !== null ||
nodeConfig.node.originalNodeObject.verbosity !== null ||
nodeConfig.node.originalNodeObject.job_tags !== null ||
nodeConfig.node.originalNodeObject.skip_tags !== null ||
nodeConfig.node.originalNodeObject.diff_mode !== null ||
showExtraVars)">
{{:: strings.get('workflow_maker.READ_ONLY_NO_PROMPT_VALUES')}}
</div>
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.job_type !== null">
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.JOB_TYPE') }}</div>
<div class="Prompt-previewRowValue">
<span ng-if="nodeConfig.node.originalNodeObject.job_type === 'run'">{{:: strings.get('prompt.PLAYBOOK_RUN') }}</span>
<span ng-if="nodeConfig.node.originalNodeObject.job_type === 'check'">{{:: strings.get('prompt.CHECK') }}</span>
</div>
</div>
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.inventory !== null">
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.INVENTORY') }}</div>
<div class="Prompt-previewRowValue" ng-bind="nodeConfig.node.originalNodeObject.summary_fields.inventory.name"></div>
</div>
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.limit !== null">
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.LIMIT') }}</div>
<div class="Prompt-previewRowValue" ng-bind="nodeConfig.node.originalNodeObject.limit"></div>
</div>
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.verbosity !== null">
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.VERBOSITY') }}</div>
<div class="Prompt-previewRowValue" ng-bind="nodeConfig.node.originalNodeObject.verbosity"></div>
</div>
<div class="Prompt-previewRow--noflex" ng-if="nodeConfig.node.originalNodeObject.job_tags !== null">
<div class="Prompt-previewRowTitle">
<span>{{:: strings.get('prompt.JOB_TAGS') }}&nbsp;</span>
<span ng-click="showJobTags = !showJobTags">
<span class="fa fa-caret-down" ng-show="showJobTags" ></span>
<span class="fa fa-caret-left" ng-show="!showJobTags"></span>
</span>
</div>
<div ng-show="showJobTags" class="Prompt-previewTagContainer">
<div class="u-wordwrap" ng-repeat="tag in jobTags">
<div class="LabelList-tag">
<span>{{tag}}</span>
</div>
</div>
</div>
</div>
<div class="Prompt-previewRow--noflex" ng-if="nodeConfig.node.originalNodeObject.skip_tags !== null">
<div class="Prompt-previewRowTitle">
<span>{{:: strings.get('prompt.SKIP_TAGS') }}&nbsp;</span>
<span ng-click="showSkipTags = !showSkipTags">
<span class="fa fa-caret-down" ng-show="showSkipTags" ></span>
<span class="fa fa-caret-left" ng-show="!showSkipTags"></span>
</span>
</div>
<div ng-show="showSkipTags" class="Prompt-previewTagContainer">
<div class="u-wordwrap" ng-repeat="tag in skipTags">
<div class="LabelList-tag">
<span>{{tag}}</span>
</div>
</div>
</div>
</div>
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.diff_mode !== null">
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.SHOW_CHANGES') }}</div>
<div class="Prompt-previewRowValue">
<span ng-if="promptData.prompts.diffMode.value">{{:: strings.get('ON') }}</span>
<span ng-if="!promptData.prompts.diffMode.value">{{:: strings.get('OFF') }}</span>
</div>
</div>
<div class="Prompt-previewRow--noflex" ng-show="showExtraVars">
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.EXTRA_VARIABLES') }}</div>
<div>
<textarea rows="6" ng-model="extraVars" name="node_form_extra_vars" class="form-control Form-textArea Form-textAreaLabel" id="workflow_node_form_extra_vars"></textarea>
</div>
</div>
</div>
<div class="buttons Form-buttons" id="workflow_maker_controls">
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton" ng-click="openPromptModal()"> {{:: strings.get('prompt.PROMPT') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="!readOnly" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>

View File

@ -309,6 +309,10 @@
color: @default-err;
}
.WorkflowMaker-readOnlyPromptText {
margin-bottom: 20px;
}
.Key-list {
margin: 0;
padding: 20px;

View File

@ -7,9 +7,12 @@
export default ['$scope', 'TemplatesService',
'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', 'WorkflowChartService',
'Wait',
function ($scope, TemplatesService,
ProcessErrors, CreateSelect2, $q, JobTemplate,
Empty, PromptService, Rest, TemplatesStrings, WorkflowChartService) {
Empty, PromptService, Rest, TemplatesStrings, WorkflowChartService,
Wait
) {
$scope.strings = TemplatesStrings;
$scope.preventCredsWithPasswords = true;
@ -90,10 +93,12 @@ export default ['$scope', 'TemplatesService',
$scope.saveWorkflowMaker = function () {
Wait('start');
if ($scope.graphState.arrayOfNodesForChart.length > 1) {
let addPromises = [];
let editPromises = [];
let credentialsToPost = [];
let credentialRequests = [];
Object.keys(nodeRef).map((workflowMakerNodeId) => {
if (nodeRef[workflowMakerNodeId].isNew) {
@ -114,8 +119,8 @@ export default ['$scope', 'TemplatesService',
});
credentialIdsToPost.forEach((credentialToPost) => {
credentialsToPost.push({
id: data.id,
credentialRequests.push({
id: data.data.id,
data: {
id: credentialToPost.id
}
@ -128,6 +133,51 @@ export default ['$scope', 'TemplatesService',
id: nodeRef[workflowMakerNodeId].originalNodeObject.id,
data: buildSendableNodeData(nodeRef[workflowMakerNodeId])
}));
if (_.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.ask_credential_on_launch')) {
let credentialsNotInPriorCredentials = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.value.filter(function (credFromPrompt) {
let defaultCreds = _.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.defaults.credentials', []);
return !defaultCreds.some(function (defaultCred) {
return credFromPrompt.id === defaultCred.id;
});
});
let credentialsToAdd = credentialsNotInPriorCredentials.filter(function (credNotInPrior) {
let previousOverrides = _.get(nodeRef[workflowMakerNodeId], 'promptData.prompts.credentials.previousOverrides', []);
return !previousOverrides.some(function (priorCred) {
return credNotInPrior.id === priorCred.id;
});
});
let credentialsToRemove = [];
if (_.has(nodeRef[workflowMakerNodeId], 'promptData.prompts.credentials.previousOverrides')) {
credentialsToRemove = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.previousOverrides.filter(function (priorCred) {
return !credentialsNotInPriorCredentials.some(function (credNotInPrior) {
return priorCred.id === credNotInPrior.id;
});
});
}
credentialsToAdd.forEach((credentialToAdd) => {
credentialRequests.push({
id: nodeRef[workflowMakerNodeId].originalNodeObject.id,
data: {
id: credentialToAdd.id
}
});
});
credentialsToRemove.forEach((credentialToRemove) => {
credentialRequests.push({
id: nodeRef[workflowMakerNodeId].originalNodeObject.id,
data: {
id: credentialToRemove.id,
disassociate: true
}
});
});
}
}
});
@ -222,7 +272,7 @@ export default ['$scope', 'TemplatesService',
case "success":
if (
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes ||
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].id)
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
) {
associatePromises.push(
TemplatesService.associateWorkflowNode({
@ -236,7 +286,7 @@ export default ['$scope', 'TemplatesService',
case "failure":
if (
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes ||
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].id)
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
) {
associatePromises.push(
TemplatesService.associateWorkflowNode({
@ -250,7 +300,7 @@ export default ['$scope', 'TemplatesService',
case "always":
if (
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes ||
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].id)
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
) {
associatePromises.push(
TemplatesService.associateWorkflowNode({
@ -267,7 +317,7 @@ export default ['$scope', 'TemplatesService',
$q.all(disassociatePromises)
.then(function () {
let credentialPromises = credentialsToPost.map(function (request) {
let credentialPromises = credentialRequests.map(function (request) {
return TemplatesService.postWorkflowNodeCredential({
id: request.id,
data: request.data
@ -276,12 +326,14 @@ export default ['$scope', 'TemplatesService',
return $q.all(associatePromises.concat(credentialPromises))
.then(function () {
Wait('stop');
$scope.closeDialog();
});
}).catch(({
data,
status
}) => {
Wait('stop');
ProcessErrors($scope, data, status, null, {});
});
});
@ -294,6 +346,7 @@ export default ['$scope', 'TemplatesService',
$q.all(deletePromises)
.then(function () {
Wait('stop');
$scope.closeDialog();
$state.transitionTo('templates');
});
@ -335,8 +388,6 @@ export default ['$scope', 'TemplatesService',
workflowMakerNodeIdCounter++;
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
$scope.$broadcast("refreshWorkflowChart");
$scope.formState.showNodeForm = true;
@ -383,8 +434,6 @@ export default ['$scope', 'TemplatesService',
workflowMakerNodeIdCounter++;
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
$scope.$broadcast("refreshWorkflowChart");
$scope.formState.showNodeForm = true;
@ -480,7 +529,6 @@ export default ['$scope', 'TemplatesService',
}
}
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
} else if ($scope.nodeConfig.mode === "edit") {
$scope.graphState.nodeBeingEdited = null;
}
@ -560,7 +608,6 @@ export default ['$scope', 'TemplatesService',
// User is going from editing one link to editing another
if ($scope.linkConfig.mode === "add") {
$scope.graphState.arrayOfLinksForChart.splice($scope.graphState.arrayOfLinksForChart.length-1, 1);
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
}
setupLinkEdit();
}
@ -603,8 +650,6 @@ export default ['$scope', 'TemplatesService',
}
});
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
$scope.graphState.isLinkMode = false;
} else {
// This is the first node selected
@ -689,8 +734,6 @@ export default ['$scope', 'TemplatesService',
}
}
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
$scope.formState.showLinkForm = false;
$scope.linkConfig = null;
$scope.$broadcast("refreshWorkflowChart");
@ -713,7 +756,6 @@ export default ['$scope', 'TemplatesService',
edgeType: "always"
});
}
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
}
$scope.graphState.linkBeingEdited = null;
$scope.graphState.addLinkSource = null;
@ -804,8 +846,6 @@ export default ['$scope', 'TemplatesService',
$scope.deleteOverlayVisible = false;
$scope.graphState.depthMap = WorkflowChartService.generateDepthMap($scope.graphState.arrayOfLinksForChart);
$scope.nodeToBeDeleted = null;
$scope.deleteOverlayVisible = false;
@ -848,7 +888,7 @@ export default ['$scope', 'TemplatesService',
let page = 1;
let getNodes = function () {
// Get the workflow nodes
Wait('start');
TemplatesService.getWorkflowJobTemplateNodes($scope.workflowJobTemplateObj.id, page)
.then(function (data) {
for (var i = 0; i < data.data.results.length; i++) {
@ -864,11 +904,12 @@ export default ['$scope', 'TemplatesService',
({arrayOfNodesForChart, arrayOfLinksForChart, chartNodeIdToIndexMapping, nodeIdToChartNodeIdMapping, nodeRef, workflowMakerNodeIdCounter} = WorkflowChartService.generateArraysOfNodesAndLinks(allNodes));
let depthMap = WorkflowChartService.generateDepthMap(arrayOfLinksForChart);
$scope.graphState = { arrayOfNodesForChart, arrayOfLinksForChart };
$scope.graphState = { arrayOfNodesForChart, arrayOfLinksForChart, depthMap };
Wait('stop');
}
}, function ({ data, status, config }) {
Wait('stop');
ProcessErrors($scope, data, status, null, {
hdr: $scope.strings.get('error.HEADER'),
msg: $scope.strings.get('error.CALL', {

View File

@ -175,9 +175,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
({arrayOfNodesForChart, arrayOfLinksForChart, chartNodeIdToIndexMapping, nodeIdToChartNodeIdMapping} = WorkflowChartService.generateArraysOfNodesAndLinks(workflowNodes));
let depthMap = WorkflowChartService.generateDepthMap(arrayOfLinksForChart);
$scope.graphState = { arrayOfNodesForChart, arrayOfLinksForChart, depthMap };
$scope.graphState = { arrayOfNodesForChart, arrayOfLinksForChart };
}
$scope.toggleStdoutFullscreen = function() {