Implements workflow convergence without proper layout

This commit is contained in:
mabashian 2018-11-06 17:04:44 -05:00
parent 07db7a41b3
commit 7b95d2114d
15 changed files with 1606 additions and 1484 deletions

View File

@ -124,9 +124,12 @@ function TemplatesStrings (BaseString) {
INVENTORY_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden by the parent workflow inventory.'),
INVENTORY_PROMPT_WILL_OVERRIDE: t.s('The inventory of this node will be overridden if a parent workflow inventory is provided at launch.'),
INVENTORY_PROMPT_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden if a parent workflow inventory is provided at launch.'),
EDIT_LINK: ({ parentName, childName }) => t.s('EDIT LINK | {{parentName}} to {{childName}}', { parentName, childName }),
VIEW_LINK: ({ parentName, childName }) => t.s('VIEW LINK | {{parentName}} to {{childName}}', { parentName, childName })
}
ADD_LINK: t.s('ADD LINK'),
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')
};
}
TemplatesStrings.$inject = ['BaseStringService'];

View File

@ -14,7 +14,6 @@ import prompt from './prompt/main';
import workflowChart from './workflows/workflow-chart/main';
import workflowMaker from './workflows/workflow-maker/main';
import workflowControls from './workflows/workflow-controls/main';
import workflowService from './workflows/workflow.service';
import WorkflowForm from './workflows.form';
import InventorySourcesList from './inventory-sources.list';
import TemplateList from './templates.list';
@ -35,7 +34,6 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
workflowChart.name, workflowMaker.name, workflowControls.name
])
.service('TemplatesService', templatesService)
.service('WorkflowService', workflowService)
.factory('WorkflowForm', WorkflowForm)
// TODO: currently being kept arround for rbac selection, templates within projects and orgs, etc.
.factory('TemplateList', TemplateList)

View File

@ -16,7 +16,7 @@
.WorkflowChart-linkHovering .WorkflowChart-linkOverlay {
cursor: pointer;
opacity: 1;
fill: @cgrey;
fill: #E1E1E1;
}
.WorkflowChart-linkHovering .WorkflowChart-linkPath {
@ -27,9 +27,11 @@
.WorkflowChart-link polygon,
.WorkflowChart-link .WorkflowChart-betweenNodesIcon,
.WorkflowChart-node .WorkflowChart-nodeAddCircle,
.WorkflowChart-node .WorkflowChart-linkCircle,
.WorkflowChart-node .WorkflowChart-nodeRemoveCircle,
.WorkflowChart-node .WorkflowChart-nodeAddIcon,
.WorkflowChart-node .WorkflowChart-nodeRemoveIcon {
.WorkflowChart-node .WorkflowChart-nodeRemoveIcon,
.WorkflowChart-node .WorkflowChart-nodeLinkIcon {
opacity: 0;
}
@ -41,6 +43,14 @@
fill: @default-succ-hov;
}
.WorkflowChart-node .WorkflowChart-linkCircle {
fill: @default-link;
}
.WorkflowChart-linkCircle.WorkflowChart-linkButtonHovering {
fill: @default-link-hov;
}
.WorkflowChart-node .WorkflowChart-nodeRemoveCircle {
fill: @default-err;
}
@ -53,20 +63,27 @@
fill: @default-secondary-bg;
}
.WorkflowChart-rect.WorkflowChart-placeholder {
.WorkflowChart-rect.WorkflowChart-isNodeBeingAdded {
stroke-dasharray: 3;
}
.WorkflowChart-node .WorkflowChart-transparentRect {
.WorkflowChart-node .WorkflowChart-nodeOverlay--transparent {
fill: @default-bg;
opacity: 0;
}
.WorkflowChart-node .WorkflowChart-nodeOverlay--disabled {
fill: @default-dark;
opacity: 0.2;
}
.WorkflowChart-alwaysShowAdd circle,
.WorkflowChart-alwaysShowAdd path,
.WorkflowChart-alwaysShowAdd .WorkflowChart-betweenNodesIcon,
.WorkflowChart-nodeHovering .WorkflowChart-nodeAddCircle,
.WorkflowChart-nodeHovering .WorkflowChart-nodeAddIcon,
.WorkflowChart-nodeHovering .WorkflowChart-linkCircle,
.WorkflowChart-nodeHovering .WorkflowChart-nodeLinkIcon,
.WorkflowChart-nodeHovering .WorkflowChart-nodeRemoveCircle,
.WorkflowChart-nodeHovering .WorkflowChart-nodeRemoveIcon,
.WorkflowChart-addHovering circle,
@ -76,7 +93,7 @@
opacity: 1;
}
.WorkflowChart-link.WorkflowChart-placeholder {
.WorkflowChart-link.WorkflowChart-isNodeBeingAdded {
stroke-dasharray: 3;
}
@ -184,3 +201,11 @@
.WorkflowChart-dashedNode {
stroke-dasharray: 5,5;
}
.WorkflowChart-nodeLinkIcon {
color: @default-bg;
}
.WorkflowChart-nodeHovering .WorkflowChart-addLinkCircle {
fill: @default-link;
}

View File

@ -4,8 +4,8 @@
* All Rights Reserved
*************************************************/
export default ['$scope', 'TemplatesStrings', 'CreateSelect2', '$timeout',
function($scope, TemplatesStrings, CreateSelect2, $timeout) {
export default ['$scope', 'TemplatesStrings', 'CreateSelect2',
function($scope, TemplatesStrings, CreateSelect2) {
$scope.strings = TemplatesStrings;
$scope.edgeTypeOptions = [

View File

@ -13,7 +13,8 @@ export default ['templateUrl',
linkConfig: '<',
readOnly: '<',
cancel: '&',
select: '&'
select: '&',
unlink: '&'
},
restrict: 'E',
templateUrl: templateUrl('templates/workflows/workflow-maker/forms/workflow-link-form'),

View File

@ -1,6 +1,8 @@
<div class="WorkflowMaker-formTitle">{{readOnly ? strings.get('workflow_maker.VIEW_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) : strings.get('workflow_maker.EDIT_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) }}</div>
<div class="WorkflowMaker-form">
<div class="form-group Form-formGroup Form-formGroup--singleColumn">
<div class="WorkflowMaker-formTitle">{{readOnly ? strings.get('workflow_maker.VIEW_LINK') : (linkConfig.mode === 'add' ? strings.get('workflow_maker.ADD_LINK') : strings.get('workflow_maker.EDIT_LINK')) }} | {{linkConfig.parent.name}} {{linkConfig.child ? 'to ' + linkConfig.child.name : ''}}</div>
<div class="WorkflowMaker-form">
<div class="form-group Form-formGroup Form-formGroup--singleColumn">
<div ng-show="linkConfig.mode === 'add' && !linkConfig.child">{{:: strings.get('workflow_maker.NEW_LINK')}}</div>
<span ng-show="linkConfig.child">
<label for="edgeType" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: strings.get('workflow_maker.RUN') }}</span>
@ -17,11 +19,12 @@
aria-hidden="true">
</select>
</div>
</div>
<div class="buttons Form-buttons" id="workflow_maker_controls">
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-show="!readOnly" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-show="readOnly" ng-click="cancel()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_link_btn" ng-show="!readOnly" ng-click="select({edgeType: edgeType.value})" ng-disabled="!edgeType"> {{:: strings.get('SAVE') }}</button>
</div>
</span>
</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_unlink_btn" ng-show="!readOnly && linkConfig.canUnlink" ng-click="unlink()"> {{:: strings.get('workflow_maker.UNLINK') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-show="!readOnly" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-show="readOnly" ng-click="cancel()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_link_btn" ng-show="!readOnly && linkConfig.child" ng-click="select({edgeType: edgeType.value})" ng-disabled="!edgeType"> {{:: strings.get('SAVE') }}</button>
</div>
</div>

View File

@ -5,11 +5,11 @@
*************************************************/
export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService', 'Rest', '$q',
'WorkflowService', 'TemplatesStrings', 'CreateSelect2', 'Empty', 'generateList', 'QuerySet',
'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList',
'TemplatesStrings', 'CreateSelect2', 'Empty', 'generateList', 'QuerySet',
'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList', 'ProcessErrors',
function($scope, TemplatesService, JobTemplate, PromptService, Rest, $q,
WorkflowService, TemplatesStrings, CreateSelect2, Empty, generateList, qs,
GetBasePath, TemplateList, ProjectList, InventorySourcesList
TemplatesStrings, CreateSelect2, Empty, generateList, qs,
GetBasePath, TemplateList, ProjectList, InventorySourcesList, ProcessErrors
) {
let promptWatcher, credentialsWatcher, surveyQuestionWatcher, listPromises = [];
@ -55,48 +55,6 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
projectList.disableRowValue = 'readOnly';
$scope.projectList = projectList;
$scope.$watch('node', (newNode, oldNode) => {
if (oldNode.id !== newNode.id) {
setupNodeForm();
}
});
$scope.$watchGroup(['templates', 'projects', 'inventory_sources', 'activeTab'], () => {
// TODO: make this more concise
switch($scope.activeTab) {
case 'jobs':
$scope.templates.forEach(function(row, i) {
if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) {
$scope.templates[i].checked = 1;
}
else {
$scope.templates[i].checked = 0;
}
});
break;
case 'project_syncs':
$scope.projects.forEach(function(row, i) {
if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) {
$scope.projects[i].checked = 1;
}
else {
$scope.projects[i].checked = 0;
}
});
break;
case 'inventory_syncs':
$scope.inventory_sources.forEach(function(row, i) {
if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) {
$scope.inventory_sources[i].checked = 1;
}
else {
$scope.inventory_sources[i].checked = 0;
}
});
break;
}
});
const checkCredentialsForRequiredPasswords = () => {
let credentialRequiresPassword = false;
$scope.promptData.prompts.credentials.value.forEach((credential) => {
@ -135,14 +93,44 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
}
};
const finishConfiguringAdd = () => {
$scope.activeTab = "jobs";
const alwaysOption = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: 'always'
};
const successOption = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: 'success'
};
const failureOption = {
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: 'failure'
};
$scope.edgeTypeOptions = [alwaysOption];
switch($scope.nodeConfig.newNodeIsRoot) {
case true:
$scope.edgeType = alwaysOption;
break;
case false:
$scope.edgeType = successOption;
$scope.edgeTypeOptions.push(successOption, failureOption);
break;
}
CreateSelect2({
element: '#workflow_node_edge_3',
multiple: false
});
$scope.nodeFormDataLoaded = true;
};
const finishConfiguringEdit = () => {
let jobTemplate = new JobTemplate();
console.log($scope.node);
if (!_.isEmpty($scope.node.promptData)) {
$scope.promptData = _.cloneDeep($scope.node.promptData);
if (_.get($scope, 'nodeConfig.node.promptData') && !_.isEmpty($scope.nodeConfig.node.promptData)) {
$scope.promptData = _.cloneDeep($scope.nodeConfig.node.promptData);
const launchConf = $scope.promptData.launchConf;
if (!launchConf.survey_enabled &&
@ -162,7 +150,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
} else {
$scope.showPromptButton = true;
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'node.originalNodeObj.summary_fields.inventory')) {
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
@ -170,13 +158,13 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
}
$scope.nodeFormDataLoaded = true;
} else if (
_.get($scope, 'node.unifiedJobTemplate.unified_job_type') === 'job_template' ||
_.get($scope, 'node.unifiedJobTemplate.type') === 'job_template'
_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.unified_job_type') === 'job_template' ||
_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.type') === 'job_template'
) {
let promises = [jobTemplate.optionsLaunch($scope.node.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.node.unifiedJobTemplate.id)];
let promises = [jobTemplate.optionsLaunch($scope.nodeConfig.node.fullUnifiedJobTemplateObject.id), jobTemplate.getLaunch($scope.nodeConfig.node.fullUnifiedJobTemplateObject.id)];
if (_.has($scope, 'node.originalNodeObj.related.credentials')) {
Rest.setUrl($scope.node.originalNodeObj.related.credentials);
if (_.has($scope, 'nodeConfig.node.originalNodeObject.related.credentials')) {
Rest.setUrl($scope.nodeConfig.node.originalNodeObject.related.credentials);
promises.push(Rest.get());
}
@ -189,7 +177,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
let prompts = PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data,
currentValues: $scope.node.originalNodeObj
currentValues: $scope.nodeConfig.node.originalNodeObject
});
let defaultCredsWithoutOverrides = [];
@ -222,7 +210,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
prompts.credentials.value = workflowNodeCredentials.concat(defaultCredsWithoutOverrides);
if ((!$scope.node.unifiedJobTemplate.inventory && !launchConf.ask_inventory_on_launch) || !$scope.node.unifiedJobTemplate.project) {
if ((!$scope.nodeConfig.node.fullUnifiedJobTemplateObject.inventory && !launchConf.ask_inventory_on_launch) || !$scope.nodeConfig.node.fullUnifiedJobTemplateObject.project) {
$scope.selectedTemplateInvalid = true;
} else {
$scope.selectedTemplateInvalid = false;
@ -264,7 +252,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
} else {
$scope.showPromptButton = true;
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'node.originalNodeObj.summary_fields.inventory')) {
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
@ -272,24 +260,24 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
if (responses[1].data.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions($scope.node.unifiedJobTemplate.id)
jobTemplate.getSurveyQuestions($scope.nodeConfig.node.fullUnifiedJobTemplateObject.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec,
extra_data: _.cloneDeep($scope.node.originalNodeObj.extra_data)
extra_data: _.cloneDeep($scope.nodeConfig.node.originalNodeObject.extra_data)
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data);
$scope.node.promptData = $scope.promptData = {
$scope.nodeConfig.node.promptData = $scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
surveyQuestions: surveyQuestionRes.data.spec,
template: $scope.node.unifiedJobTemplate.id
template: $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id
};
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
@ -309,11 +297,11 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
$scope.nodeFormDataLoaded = true;
});
} else {
$scope.node.promptData = $scope.promptData = {
$scope.nodeConfig.node.promptData = $scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
template: $scope.node.unifiedJobTemplate.id
template: $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id
};
checkCredentialsForRequiredPasswords();
@ -328,12 +316,12 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
$scope.nodeFormDataLoaded = true;
}
if (_.get($scope, 'node.unifiedJobTemplate')) {
if (_.get($scope, 'node.unifiedJobTemplate.type') === "job_template") {
if (_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject')) {
if (_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.type') === "job_template") {
$scope.activeTab = "jobs";
}
$scope.selectedTemplate = $scope.node.unifiedJobTemplate;
$scope.selectedTemplate = $scope.nodeConfig.node.fullUnifiedJobTemplateObject;
if ($scope.selectedTemplate.unified_job_type) {
switch ($scope.selectedTemplate.unified_job_type) {
@ -364,79 +352,6 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
$scope.activeTab = "jobs";
}
if ($scope.mode === 'add') {
const alwaysOption = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: 'always'
};
const successOption = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: 'success'
};
const failureOption = {
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: 'failure'
};
$scope.edgeTypeOptions = [alwaysOption];
switch($scope.node.isRoot) {
case true:
$scope.edgeType = alwaysOption;
break;
case false:
$scope.edgeType = successOption;
$scope.edgeTypeOptions.push(successOption, failureOption);
break;
}
CreateSelect2({
element: '#workflow_node_edge_3',
multiple: false
});
$scope.nodeFormDataLoaded = true;
}
};
// Determine whether or not we need to go out and GET this nodes unified job template
// in order to determine whether or not prompt fields are needed
$scope.openPromptModal = function() {
$scope.promptData.triggerModalOpen = true;
};
$scope.toggle_row = function(selectedRow) {
if (!$scope.readOnly) {
// TODO: make this more concise
switch($scope.activeTab) {
case 'jobs':
$scope.templates.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.templates[i].checked = 1;
} else {
$scope.templates[i].checked = 0;
}
});
break;
case 'project_syncs':
$scope.projects.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.projects[i].checked = 1;
} else {
$scope.projects[i].checked = 0;
}
});
break;
case 'inventory_syncs':
$scope.inventory_sources.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.inventory_sources[i].checked = 1;
} else {
$scope.inventory_sources[i].checked = 0;
}
});
break;
}
templateManuallySelected(selectedRow);
}
};
const templateManuallySelected = (selectedTemplate) => {
@ -597,7 +512,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
page_size: '5',
order_by: 'name',
not__source: ''
}
};
$scope.inventory_sources = [];
$scope.inventory_source_dataset = {};
@ -612,26 +527,113 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
$q.all(listPromises)
.then(() => {
if (!$scope.node.isNew && !$scope.node.edited && $scope.node.unifiedJobTemplate && $scope.node.unifiedJobTemplate.unified_job_type && $scope.node.unifiedJobTemplate.unified_job_type === 'job') {
// This is a node that we got back from the api with an incomplete
// unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.node.unifiedJobTemplate.id)
.then(function(data) {
$scope.node.unifiedJobTemplate = _.clone(data.data.results[0]);
finishConfiguringEdit();
}, function(error) {
ProcessErrors($scope, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to get unified job template. GET returned ' +
'status: ' + error.status
if ($scope.nodeConfig.mode === "edit") {
// Make sure that we have the full unified job template object
if (!$scope.nodeConfig.node.fullUnifiedJobTemplate && _.get($scope, 'nodeConfig.node.originalNodeObject.summary_fields.unified_job_template.unified_job_type') === 'job') {
// This is a node that we got back from the api with an incomplete
// unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.nodeConfig.node.originalNodeObject.summary_fields.unified_job_template.id)
.then(function({data}) {
$scope.nodeConfig.node.fullUnifiedJobTemplateObject = data.results[0];
finishConfiguringEdit();
}, function(error) {
ProcessErrors($scope, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to get unified job template. GET returned ' +
'status: ' + error.status
});
});
});
} else {
finishConfiguringEdit();
}
} else {
finishConfiguringEdit();
finishConfiguringAdd();
}
});
}
};
$scope.openPromptModal = function() {
$scope.promptData.triggerModalOpen = true;
};
$scope.toggle_row = function(selectedRow) {
if (!$scope.readOnly) {
// TODO: make this more concise
switch($scope.activeTab) {
case 'jobs':
$scope.templates.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.templates[i].checked = 1;
} else {
$scope.templates[i].checked = 0;
}
});
break;
case 'project_syncs':
$scope.projects.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.projects[i].checked = 1;
} else {
$scope.projects[i].checked = 0;
}
});
break;
case 'inventory_syncs':
$scope.inventory_sources.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.inventory_sources[i].checked = 1;
} else {
$scope.inventory_sources[i].checked = 0;
}
});
break;
}
templateManuallySelected(selectedRow);
}
};
$scope.$watch('nodeConfig.nodeId', (newNodeId, oldNodeId) => {
if (newNodeId !== oldNodeId) {
setupNodeForm();
}
});
$scope.$watchGroup(['templates', 'projects', 'inventory_sources', 'activeTab'], () => {
// TODO: make this more concise
switch($scope.activeTab) {
case 'jobs':
$scope.templates.forEach(function(row, i) {
if(_.hasIn($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.id') && row.id === $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) {
$scope.templates[i].checked = 1;
}
else {
$scope.templates[i].checked = 0;
}
});
break;
case 'project_syncs':
$scope.projects.forEach(function(row, i) {
if(_.hasIn($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.id') && row.id === $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) {
$scope.projects[i].checked = 1;
}
else {
$scope.projects[i].checked = 0;
}
});
break;
case 'inventory_syncs':
$scope.inventory_sources.forEach(function(row, i) {
if(_.hasIn($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.id') && row.id === $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) {
$scope.inventory_sources[i].checked = 1;
}
else {
$scope.inventory_sources[i].checked = 0;
}
});
break;
}
});
setupNodeForm();
}

View File

@ -10,8 +10,7 @@ export default ['templateUrl',
function(templateUrl) {
return {
scope: {
mode: '<',
node: '=',
nodeConfig: '<',
cancel: '&',
select: '&',
readOnly: '<'

View File

@ -1,5 +1,5 @@
<div ng-show="nodeFormDataLoaded">
<div class="WorkflowMaker-formTitle ng-binding">{{mode === 'edit' ? node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}</div>
<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="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>
@ -118,7 +118,7 @@
<span>{{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}</span>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="mode === 'add'">
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="nodeConfig.mode === 'add'">
<label for="edgeType" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: strings.get('workflow_maker.RUN') }}</span>

View File

@ -74,26 +74,39 @@
</div>
<div class="WorkflowLegend-maker--right">
<span class="WorkflowMaker-totalJobs">{{strings.get('workflow_maker.TOTAL_TEMPLATES')}}</span>
<span class="badge List-titleBadge" ng-bind="treeData.data.totalNodes"></span>
<span class="badge List-titleBadge" ng-bind="treeState.arrayOfNodesForChart.length-1"></span>
<i ng-class="{'WorkflowMaker-manualControlsIcon--active': showManualControls}" class="fa fa-cog WorkflowMaker-manualControlsIcon" aria-hidden="true" alt="Controls" ng-click="toggleManualControls()"></i>
<div ng-show="showManualControls" class="WorkflowMaker-manualControls noselect">
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()" zoom-to-fit-chart="zoomToFitChart()"></workflow-controls>
</div>
</div>
</div>
<workflow-chart ng-if="modalOpen" tree-data="treeData.data" add-node="startAddNode(parent, betweenTwoNodes)" edit-node="startEditNode(nodeToEdit)" edit-link="startEditLink(parentId, childId)" delete-node="startDeleteNode(nodeToDelete)" workflow-zoomed="workflowZoomed(zoom)" can-add-workflow-job-template="canAddWorkflowJobTemplate" workflow-job-template-obj="workflowJobTemplateObj" mode="edit" class="WorkflowMaker-chart"></workflow-chart>
<workflow-chart
ng-if="modalOpen"
tree-state="treeState"
add-node-without-child="startAddNodeWithoutChild(parent)"
add-node-with-child="startAddNodeWithChild(link)"
edit-node="startEditNode(nodeToEdit)"
edit-link="startEditLink(linkToEdit)"
select-node-for-linking="selectNodeForLinking(nodeToStartLink)"
delete-node="startDeleteNode(nodeToDelete)"
workflow-zoomed="workflowZoomed(zoom)"
read-only="readOnly"
mode="edit"
class="WorkflowMaker-chart">
</workflow-chart>
</div>
<div class="WorkflowMaker-contentRight">
<span ng-if="formState.showNodeForm">
<workflow-node-form mode="nodeFormMode" node="nodeBeingWorkedOn" select="confirmNodeForm(selectedTemplate, promptData, edgeType)" cancel="cancelNodeForm()" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"/>
<workflow-node-form node-config="nodeConfig" node="nodeBeingWorkedOn" select="confirmNodeForm(selectedTemplate, promptData, edgeType)" cancel="cancelNodeForm()" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"/>
</span>
<span ng-if="formState.showLinkForm">
<workflow-link-form link-config="linkConfig" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit" select="confirmLinkForm(edgeType)" cancel="cancelLinkForm()"/>
<workflow-link-form link-config="linkConfig" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit" select="confirmLinkForm(edgeType)" cancel="cancelLinkForm()" unlink="unlink()"/>
</span>
</div>
</div>
<div class="WorkflowMaker-buttonHolder">
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="showNodeForm || showLinkForm"> {{:: strings.get('SAVE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="formState.showNodeForm || formState.showLinkForm"> {{:: strings.get('SAVE') }}</button>
</div>
</div>

View File

@ -1,294 +0,0 @@
export default ['$q', function($q){
return {
searchTree: function(params) {
// params.element
// params.matchingId
// params.byNodeId
let prospectiveId = params.byNodeId ? params.element.nodeId : params.element.id;
if(prospectiveId === params.matchingId){
return params.element;
}else if (params.element.children && params.element.children.length > 0){
let result = null;
const thisService = this;
_.forEach(params.element.children, function(child) {
result = thisService.searchTree({
element: child,
matchingId: params.matchingId,
byNodeId: params.byNodeId ? params.byNodeId : false
});
if(result) {
return false;
}
});
return result;
}
return null;
},
removeNodeFromTree: function(params) {
// params.tree
// params.nodeToBeDeleted
let parentNode = this.searchTree({
element: params.tree,
matchingId: params.nodeToBeDeleted.parent.id
});
let nodeToBeDeleted = this.searchTree({
element: parentNode,
matchingId: params.nodeToBeDeleted.id
});
if(nodeToBeDeleted.children) {
_.forEach(nodeToBeDeleted.children, function(child) {
if(nodeToBeDeleted.isRoot) {
child.isRoot = true;
child.edgeType = "always";
}
child.parent = parentNode;
parentNode.children.push(child);
});
}
_.forEach(parentNode.children, function(child, index) {
if(child.id === params.nodeToBeDeleted.id) {
parentNode.children.splice(index, 1);
return false;
}
});
},
addPlaceholderNode: function(params) {
// params.parent
// params.betweenTwoNodes
// params.tree
// params.id
let placeholder = {
children: [],
c: "#D7D7D7",
id: params.id,
canDelete: true,
canEdit: false,
canAddTo: true,
placeholder: true,
isNew: true,
edited: false,
isRoot: (params.betweenTwoNodes) ? _.get(params, 'parent.source.isStartNode', false) : _.get(params, 'parent.isStartNode', false)
};
let parentNode = (params.betweenTwoNodes) ? this.searchTree({element: params.tree, matchingId: params.parent.source.id}) : this.searchTree({element: params.tree, matchingId: params.parent.id});
let placeholderRef;
if (params.betweenTwoNodes) {
_.forEach(parentNode.children, function(child, index) {
if (child.id === params.parent.target.id) {
child.isRoot = false;
placeholder.children.push(child);
parentNode.children[index] = placeholder;
placeholderRef = parentNode.children[index];
child.parent = parentNode.children[index];
return false;
}
});
} else {
if (parentNode.children) {
parentNode.children.push(placeholder);
placeholderRef = parentNode.children[parentNode.children.length - 1];
} else {
parentNode.children = [placeholder];
placeholderRef = parentNode.children[0];
}
}
return placeholderRef;
},
getSiblingConnectionTypes: function(params) {
// params.parentId
// params.childId
// params.tree
let siblingConnectionTypes = {};
let parentNode = this.searchTree({
element: params.tree,
matchingId: params.parentId
});
if(parentNode.children && parentNode.children.length > 0) {
// Loop across them and add the types as keys to siblingConnectionTypes
_.forEach(parentNode.children, function(child) {
if(child.id !== params.childId && !child.placeholder && child.edgeType) {
siblingConnectionTypes[child.edgeType] = true;
}
});
}
return Object.keys(siblingConnectionTypes);
},
buildTree: function(params) {
//params.workflowNodes
let deferred = $q.defer();
let _this = this;
let treeData = {
data: {
id: 1,
canDelete: false,
canEdit: false,
canAddTo: true,
isStartNode: true,
unifiedJobTemplate: {
name: "Workflow Launch"
},
children: [],
deletedNodes: [],
totalNodes: 0
},
nextIndex: 2
};
let nodesArray = params.workflowNodes;
let nodesObj = {};
let nonRootNodeIds = [];
let allNodeIds = [];
// Determine which nodes are root nodes
_.forEach(nodesArray, function(node) {
nodesObj[node.id] = _.clone(node);
allNodeIds.push(node.id);
_.forEach(node.success_nodes, function(nodeId){
nonRootNodeIds.push(nodeId);
});
_.forEach(node.failure_nodes, function(nodeId){
nonRootNodeIds.push(nodeId);
});
_.forEach(node.always_nodes, function(nodeId){
nonRootNodeIds.push(nodeId);
});
});
let rootNodes = _.difference(allNodeIds, nonRootNodeIds);
// Loop across the root nodes and re-build the tree
_.forEach(rootNodes, function(rootNodeId) {
let branch = _this.buildBranch({
nodeId: rootNodeId,
edgeType: "always",
nodesObj: nodesObj,
isRoot: true,
treeData: treeData
});
treeData.data.children.push(branch);
});
deferred.resolve(treeData);
return deferred.promise;
},
buildBranch: function(params) {
// params.nodeId
// params.parentId
// params.edgeType
// params.nodesObj
// params.isRoot
// params.treeData
let _this = this;
let treeNode = {
children: [],
c: "#D7D7D7",
id: params.treeData.nextIndex,
nodeId: params.nodeId,
canDelete: true,
canEdit: true,
canAddTo: true,
placeholder: false,
edgeType: params.edgeType,
isNew: false,
edited: false,
originalEdge: params.edgeType,
originalNodeObj: _.clone(params.nodesObj[params.nodeId]),
promptValues: {},
isRoot: params.isRoot ? params.isRoot : false
};
params.treeData.data.totalNodes++;
params.treeData.nextIndex++;
if(params.parentId) {
treeNode.originalParentId = params.parentId;
}
if(params.nodesObj[params.nodeId].summary_fields) {
if(params.nodesObj[params.nodeId].summary_fields.job) {
treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job);
}
if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) {
treeNode.unifiedJobTemplate = _.clone(params.nodesObj[params.nodeId].summary_fields.unified_job_template);
}
}
// Loop across the success nodes and add them recursively
_.forEach(params.nodesObj[params.nodeId].success_nodes, function(successNodeId) {
treeNode.children.push(_this.buildBranch({
nodeId: successNodeId,
parentId: params.nodeId,
edgeType: "success",
nodesObj: params.nodesObj,
treeData: params.treeData
}));
});
// failure nodes
_.forEach(params.nodesObj[params.nodeId].failure_nodes, function(failureNodesId) {
treeNode.children.push(_this.buildBranch({
nodeId: failureNodesId,
parentId: params.nodeId,
edgeType: "failure",
nodesObj: params.nodesObj,
treeData: params.treeData
}));
});
// always nodes
_.forEach(params.nodesObj[params.nodeId].always_nodes, function(alwaysNodesId) {
treeNode.children.push(_this.buildBranch({
nodeId: alwaysNodesId,
parentId: params.nodeId,
edgeType: "always",
nodesObj: params.nodesObj,
treeData: params.treeData
}));
});
return treeNode;
},
updateStatusOfNode: function(params) {
// params.treeData
// params.nodeId
// params.status
let matchingNode = this.searchTree({
element: params.treeData.data,
matchingId: params.nodeId,
byNodeId: true
});
if(matchingNode) {
matchingNode.job = {
status: params.status,
id: params.unified_job_id
};
}
},
};
}];

View File

@ -1,10 +1,13 @@
export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
'jobLabels', 'workflowNodes', '$scope', 'ParseTypeChange',
'ParseVariableString', 'WorkflowService', 'count', '$state', 'i18n',
'moment', '$filter', function(workflowData, workflowResultsService,
'ParseVariableString', 'count', '$state', 'i18n',
'moment', function(workflowData, workflowResultsService,
workflowDataOptions, jobLabels, workflowNodes, $scope, ParseTypeChange,
ParseVariableString, WorkflowService, count, $state, i18n, moment, $filter) {
ParseVariableString, count, $state, i18n, moment) {
var runTimeElapsedTimer = null;
let workflowMakerNodeIdCounter = 1;
let nodeIdToMakerIdMapping = {};
let chartNodeIdToIndexMapping = {};
var getLinks = function() {
var getLink = function(key) {
@ -113,11 +116,8 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
$scope.workflow_nodes = workflowNodes;
$scope.workflowOptions = workflowDataOptions.actions.GET;
$scope.labels = jobLabels;
$scope.count = count.val;
$scope.showManualControls = false;
$scope.showKey = false;
$scope.toggleKey = () => $scope.showKey = !$scope.showKey;
$scope.keyClassList = `{ 'Key-menuIcon--active': showKey }`;
$scope.readOnly = true;
// Start elapsed time updater for job known to be running
if ($scope.workflow.started !== null && $scope.workflow.status === 'running') {
@ -167,25 +167,96 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
$scope.varsTooltip= i18n._('Read only view of extra variables added to the workflow.');
$scope.varsLabel = i18n._('Extra Variables');
// Click binding for the expand/collapse button on the standard out log
$scope.stdoutFullScreen = false;
WorkflowService.buildTree({
workflowNodes: workflowNodes
}).then(function(data){
$scope.treeData = data;
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
// this treeData object and removing the children object for some reason (?)
// This happens on occasion and I think is a race condition (?)
if(!$scope.treeData.data.children) {
$scope.treeData.data.children = [];
let nonRootNodeIds = [];
let allNodeIds = [];
let arrayOfLinksForChart = [];
let arrayOfNodesForChart = [
{
index: 0,
id: workflowMakerNodeIdCounter,
isStartNode: true,
unifiedJobTemplate: {
name: "START"
},
fixed: true,
x: 0,
y: 0
}
];
$scope.canAddWorkflowJobTemplate = false;
workflowMakerNodeIdCounter++;
// Assign each node an ID - 0 is reserved for the start node. We need to
// make sure that we have an ID on every node including new nodes so the
// ID returned by the api won't do
workflowNodes.forEach((node) => {
node.workflowMakerNodeId = workflowMakerNodeIdCounter;
const nodeObj = {
index: workflowMakerNodeIdCounter-1,
id: workflowMakerNodeIdCounter,
unifiedJobTemplate: node.summary_fields.unified_job_template
};
if(node.summary_fields.job) {
nodeObj.job = node.summary_fields.job;
}
if(node.summary_fields.unified_job_template) {
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
}
arrayOfNodesForChart.push(nodeObj);
allNodeIds.push(node.id);
nodeIdToMakerIdMapping[node.id] = node.workflowMakerNodeId;
chartNodeIdToIndexMapping[workflowMakerNodeIdCounter] = workflowMakerNodeIdCounter-1;
workflowMakerNodeIdCounter++;
});
workflowNodes.forEach((node) => {
const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId];
node.success_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "success"
});
nonRootNodeIds.push(nodeId);
});
node.failure_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "failure"
});
nonRootNodeIds.push(nodeId);
});
node.always_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
nonRootNodeIds.push(nodeId);
});
});
let uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds));
let rootNodes = _.difference(allNodeIds, uniqueNonRootNodeIds);
rootNodes.forEach((rootNodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[rootNodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[0],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
});
$scope.treeState = { arrayOfNodesForChart, arrayOfLinksForChart };
}
$scope.toggleStdoutFullscreen = function() {
@ -285,12 +356,11 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateWorkflowJobElapsedTimer);
}
WorkflowService.updateStatusOfNode({
treeData: $scope.treeData,
nodeId: data.workflow_node_id,
status: data.status,
unified_job_id: data.unified_job_id
});
$scope.treeState.arrayOfNodesForChart[chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[data.workflow_node_id]]].job = {
id: data.unified_job_id,
status: data.status
};
$scope.workflow_nodes.forEach(node => {
if(parseInt(node.id) === parseInt(data.workflow_node_id)){
@ -300,8 +370,6 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
}
});
$scope.count = workflowResultsService
.getCounts($scope.workflow_nodes);
$scope.$broadcast("refreshWorkflowChart");
}
getLabelsAndTooltips();

View File

@ -363,7 +363,14 @@
</div>
</div>
</div>
<workflow-chart tree-data="treeData.data" workflow-zoomed="workflowZoomed(zoom)" can-add-workflow-job-template="canAddWorkflowJobTemplate" mode="details" class="WorkflowMaker-chart"></workflow-chart>
<workflow-chart
tree-state="treeState"
workflow-zoomed="workflowZoomed(zoom)"
can-add-workflow-job-template="canAddWorkflowJobTemplate"
mode="details"
read-only="readOnly"
class="WorkflowMaker-chart">
</workflow-chart>
</div>
</div>