diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index d3c963b7e4..ffc36b32f0 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -117,7 +117,7 @@ function TemplatesStrings (BaseString) { TOTAL_NODES: t.s('TOTAL NODES'), ADD_A_NODE: t.s('ADD A NODE'), EDIT_TEMPLATE: t.s('EDIT TEMPLATE'), - JOBS: t.s('JOBS'), + JOBS: t.s('Jobs'), PLEASE_CLICK_THE_START_BUTTON: t.s('Please click the start button to build your workflow.'), PLEASE_HOVER_OVER_A_TEMPLATE: t.s('Please hover over a template for additional options.'), EDIT_LINK_TOOLTIP: t.s('Click to edit link'), @@ -144,7 +144,8 @@ function TemplatesStrings (BaseString) { UNSAVED_CHANGES_PROMPT_TEXT: t.s('Are you sure you want to exit the Workflow Creator without saving your changes?'), EXIT: t.s('EXIT'), CANCEL: t.s('CANCEL'), - SAVE_AND_EXIT: t.s('SAVE & EXIT') + SAVE_AND_EXIT: t.s('SAVE & EXIT'), + PAUSE_NODE: t.s('Pause Node') }; } diff --git a/awx/ui/client/legacy/styles/ansible-ui.less b/awx/ui/client/legacy/styles/ansible-ui.less index 90824e43f5..708cc44496 100644 --- a/awx/ui/client/legacy/styles/ansible-ui.less +++ b/awx/ui/client/legacy/styles/ansible-ui.less @@ -1981,11 +1981,6 @@ tr td button i { box-shadow: none !important; } -.select2-container { - margin-left: 2px; - margin-top: 2px; -} - .form-control + .select2-container--disabled .select2-selection { background-color: @ebgrey !important; } diff --git a/awx/ui/client/lib/components/_index.less b/awx/ui/client/lib/components/_index.less index 81dc40c580..ea4da9a9c9 100644 --- a/awx/ui/client/lib/components/_index.less +++ b/awx/ui/client/lib/components/_index.less @@ -1,4 +1,5 @@ @import 'action/_index'; +@import 'approvalsDrawer/_index'; @import 'dialog/_index'; @import 'input/_index'; @import 'launchTemplateButton/_index'; diff --git a/awx/ui/client/lib/components/approvalsDrawer/_index.less b/awx/ui/client/lib/components/approvalsDrawer/_index.less new file mode 100644 index 0000000000..abca0e2166 --- /dev/null +++ b/awx/ui/client/lib/components/approvalsDrawer/_index.less @@ -0,0 +1,56 @@ +.at-ApprovalsDrawer { + position: absolute; + right: 0px; + top: 0; + height: 100%; + width: 540px; + background-color: white; + z-index: 1000000; + animation-duration: 0.5s; + // TODO: fix animation? + // animation-name: slidein; + padding: 20px; + box-shadow: -3px 0px 8px -2px #aaaaaa; + + &-header { + display: flex; + width: 100%; + margin-bottom: 20px; + } + + &-title { + flex: 1 0 auto; + color: #606060; + font-size: 14px; + font-weight: bold; + width: calc(82%); + } + + &-exit { + justify-content: flex-end; + display: flex; + + button { + height: 20px; + font-size: 20px; + color: #D7D7D7; + line-height: 1; + opacity: 1; + } + + button:hover{ + color: @default-icon; + opacity: 1; + } + } +} + +@keyframes slidein { + from { + width: 0px; + } + + to { + width: 540px; + } +} \ No newline at end of file diff --git a/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js b/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js new file mode 100644 index 0000000000..556d216559 --- /dev/null +++ b/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js @@ -0,0 +1,86 @@ +const templateUrl = require('~components/approvalsDrawer/approvalsDrawer.partial.html'); + +function AtApprovalsDrawerController (strings, Rest, GetBasePath, $rootScope) { + const vm = this || {}; + + const toolbarSortDefault = { + label: `${strings.get('sort.CREATED_ASCENDING')}`, + value: 'created' + }; + + vm.toolbarSortValue = toolbarSortDefault; + + // This will probably need to be expanded + vm.toolbarSortOptions = [ + toolbarSortDefault, + { label: `${strings.get('sort.CREATED_DESCENDING')}`, value: '-created' } + ]; + + vm.queryset = { + page_size: 5 + }; + + vm.emptyListReason = strings.get('approvals.NONE'); + + const loadTheList = () => { + Rest.setUrl(`${GetBasePath('workflow_approval')}?page_size=5&order_by=created&status=pending`); + Rest.get() + .then(({ data }) => { + vm.dataset = data; + vm.approvals = data.results; + vm.count = data.count; + $rootScope.pendingApprovalCount = data.count; + vm.listLoaded = true; + }); + }; + + loadTheList(); + + vm.onToolbarSort = (sort) => { + vm.toolbarSortValue = sort; + + // TODO: this... + // const queryParams = Object.assign( + // {}, + // $state.params.user_search, + // paginateQuerySet, + // { order_by: sort.value } + // ); + + // // Update URL with params + // $state.go('.', { + // user_search: queryParams + // }, { notify: false, location: 'replace' }); + + // rather than ^^ we want to just re-load the data based on new params + }; + + vm.approve = (approval) => { + Rest.setUrl(`${GetBasePath('workflow_approval')}${approval.id}/approve`); + Rest.post() + .then(() => loadTheList()); + }; + + vm.deny = (approval) => { + Rest.setUrl(`${GetBasePath('workflow_approval')}${approval.id}/deny`); + Rest.post() + .then(() => loadTheList()); + }; +} + +AtApprovalsDrawerController.$inject = ['ComponentsStrings', 'Rest', 'GetBasePath', '$rootScope']; + +function atApprovalsDrawer () { + return { + restrict: 'E', + transclude: true, + templateUrl, + controller: AtApprovalsDrawerController, + controllerAs: 'vm', + scope: { + closeApprovals: '&' + }, + }; +} + +export default atApprovalsDrawer; diff --git a/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.partial.html b/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.partial.html new file mode 100644 index 0000000000..55e8a0ab2f --- /dev/null +++ b/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.partial.html @@ -0,0 +1,79 @@ +
+
+
+
+ + NOTIFICATIONS + + + {{vm.count}} + +
+
+ +
+
+ + + + +
+
+
+ + + +
+
+
+ + + + + +
+
+
+
Continue workflow job?
+ + +
+
+
+
+
+ + +
\ No newline at end of file diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index 8ca1e26115..772ce84f37 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -119,6 +119,10 @@ function ComponentsStrings (BaseString) { EXPANDED: t.s('Expanded'), SORT_BY: t.s('SORT BY') }; + + ns.approvals = { + NONE: t.s('There are no jobs awaiting approval') + }; } ComponentsStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 1e5767fd36..cc3cd55bb0 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -2,6 +2,7 @@ import atLibServices from '~services'; import actionGroup from '~components/action/action-group.directive'; import actionButton from '~components/action/action-button.directive'; +import approvalsDrawer from '~components/approvalsDrawer/approvalsDrawer.directive'; import dialog from '~components/dialog/dialog.component'; import divider from '~components/utility/divider.directive'; import dynamicSelect from '~components/input/dynamic-select.directive'; @@ -60,6 +61,7 @@ angular ]) .directive('atActionGroup', actionGroup) .directive('atActionButton', actionButton) + .directive('atApprovalsDrawer', approvalsDrawer) .component('atDialog', dialog) .directive('atDivider', divider) .directive('atDynamicSelect', dynamicSelect) diff --git a/awx/ui/client/lib/components/layout/_index.less b/awx/ui/client/lib/components/layout/_index.less index f1ee8c2ac3..ef22c873e1 100644 --- a/awx/ui/client/lib/components/layout/_index.less +++ b/awx/ui/client/lib/components/layout/_index.less @@ -81,6 +81,23 @@ opacity: 0; } } + + .at-Layout-topNavApprovals { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + + div { + margin-left: 10px; + padding: 5px; + border-radius: 3px; + background-color: red; + color: white; + height: 15px; + font-size: 10px; + } + } } &-sideContainer { diff --git a/awx/ui/client/lib/components/layout/layout.directive.js b/awx/ui/client/lib/components/layout/layout.directive.js index 7d1f457f57..a5d9332f2a 100644 --- a/awx/ui/client/lib/components/layout/layout.directive.js +++ b/awx/ui/client/lib/components/layout/layout.directive.js @@ -25,6 +25,10 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions } }); + $scope.$watch('$root.pendingApprovalCount', () => { + vm.approvalsCount = _.get($scope, '$root.pendingApprovalCount') || 0; + }); + $scope.$watch('$root.socketStatus', (newStatus) => { vm.socketState = newStatus; vm.socketIconClass = `icon-socket-${vm.socketState}`; @@ -42,6 +46,14 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions } }; + vm.openApprovals = () => { + vm.showApprovals = true; + }; + + vm.closeApprovals = () => { + vm.showApprovals = false; + }; + function checkOrgAdmin () { const usersPath = `/api/v2/users/${vm.currentUserId}/admin_of_organizations/`; $http.get(usersPath) diff --git a/awx/ui/client/lib/components/layout/layout.partial.html b/awx/ui/client/lib/components/layout/layout.partial.html index d282ade9f1..3d36b064c6 100644 --- a/awx/ui/client/lib/components/layout/layout.partial.html +++ b/awx/ui/client/lib/components/layout/layout.partial.html @@ -14,6 +14,12 @@ {{ $parent.layoutVm.currentUsername }} + +
+ +
{{vm.approvalsCount}}
+
+
@@ -104,4 +110,5 @@ + diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index a851d1f29f..6d8cc142e9 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -1,7 +1,7 @@ import atLibServices from '~services'; -import Application from '~models/Application'; import AdHocCommand from '~models/AdHocCommand'; +import Application from '~models/Application'; import Base from '~models/Base'; import Config from '~models/Config'; import Credential from '~models/Credential'; @@ -19,16 +19,16 @@ import Me from '~models/Me'; import NotificationTemplate from '~models/NotificationTemplate'; import Organization from '~models/Organization'; import Project from '~models/Project'; -import Schedule from '~models/Schedule'; import ProjectUpdate from '~models/ProjectUpdate'; +import Schedule from '~models/Schedule'; import SystemJob from '~models/SystemJob'; import Token from '~models/Token'; +import UnifiedJob from '~models/UnifiedJob'; import UnifiedJobTemplate from '~models/UnifiedJobTemplate'; +import User from '~models/User'; import WorkflowJob from '~models/WorkflowJob'; import WorkflowJobTemplate from '~models/WorkflowJobTemplate'; import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode'; -import UnifiedJob from '~models/UnifiedJob'; -import User from '~models/User'; import ModelsStrings from '~models/models.strings'; @@ -38,8 +38,8 @@ angular .module(MODULE_NAME, [ atLibServices ]) - .service('ApplicationModel', Application) .service('AdHocCommandModel', AdHocCommand) + .service('ApplicationModel', Application) .service('BaseModel', Base) .service('ConfigModel', Config) .service('CredentialModel', Credential) @@ -54,19 +54,19 @@ angular .service('JobModel', Job) .service('JobTemplateModel', JobTemplate) .service('MeModel', Me) + .service('ModelsStrings', ModelsStrings) .service('NotificationTemplate', NotificationTemplate) .service('OrganizationModel', Organization) .service('ProjectModel', Project) - .service('ScheduleModel', Schedule) - .service('UnifiedJobModel', UnifiedJob) .service('ProjectUpdateModel', ProjectUpdate) + .service('ScheduleModel', Schedule) .service('SystemJobModel', SystemJob) .service('TokenModel', Token) + .service('UnifiedJobModel', UnifiedJob) .service('UnifiedJobTemplateModel', UnifiedJobTemplate) + .service('UserModel', User) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) - .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) - .service('UserModel', User) - .service('ModelsStrings', ModelsStrings); + .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode); export default MODULE_NAME; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 65d08e16dd..5c94436659 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -161,16 +161,16 @@ angular // }) } ]) - .run(['$stateExtender', '$q', '$compile', '$cookies', '$rootScope', '$log', '$stateParams', + .run(['$q', '$cookies', '$rootScope', '$log', '$stateParams', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', - 'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest', - 'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService', - '$filter', 'SocketService', 'AppStrings', '$transitions', - function($stateExtender, $q, $compile, $cookies, $rootScope, $log, $stateParams, + 'LoadConfig', 'Store', 'pendoService', 'Rest', + '$state', 'GetBasePath', 'ConfigService', + 'SocketService', 'AppStrings', '$transitions', + function($q, $cookies, $rootScope, $log, $stateParams, CheckLicense, $location, Authorization, LoadBasePaths, Timer, - LoadConfig, Store, pendoService, Prompt, Rest, Wait, - ProcessErrors, $state, GetBasePath, ConfigService, - $filter, SocketService, AppStrings, $transitions) { + LoadConfig, Store, pendoService, Rest, + $state, GetBasePath, ConfigService, + SocketService, AppStrings, $transitions) { $rootScope.$state = $state; $rootScope.$state.matches = function(stateName) { @@ -387,6 +387,15 @@ angular } }); }); + + Rest.setUrl(`${GetBasePath('workflow_approval')}?status=pending&page_size=1`); + Rest.get() + .then(({data}) => { + $rootScope.pendingApprovalCount = data.count; + }) + .catch(() => { + // TODO: handle this + }); } } diff --git a/awx/ui/client/src/login/loginModal/loginModal.controller.js b/awx/ui/client/src/login/loginModal/loginModal.controller.js index 1d92ae0f94..e5df9bf1b6 100644 --- a/awx/ui/client/src/login/loginModal/loginModal.controller.js +++ b/awx/ui/client/src/login/loginModal/loginModal.controller.js @@ -39,14 +39,14 @@ * This is usage information. */ -export default ['$log', '$cookies', '$compile', '$rootScope', +export default ['$log', '$cookies', '$rootScope', '$location', 'Authorization', 'Alert', 'Wait', 'Timer', 'Empty', '$scope', 'pendoService', 'ConfigService', - 'CheckLicense', 'SocketService', - function ($log, $cookies, $compile, $rootScope, $location, - Authorization, Alert, Wait, Timer, Empty, - scope, pendoService, ConfigService, CheckLicense, - SocketService) { + 'CheckLicense', 'SocketService', 'Rest', 'GetBasePath', + function ($log, $cookies, $rootScope, + $location, Authorization, Alert, Wait, Timer, + Empty, scope, pendoService, ConfigService, + CheckLicense, SocketService, Rest, GetBasePath) { var lastPath, lastUser, sessionExpired, loginAgain, preAuthUrl; loginAgain = function() { @@ -139,6 +139,15 @@ export default ['$log', '$cookies', '$compile', '$rootScope', Alert('Error', 'Failed to access user information. GET returned status: ' + status, 'alert-danger', loginAgain); }); }); + + Rest.setUrl(`${GetBasePath('workflow_approval')}?status=pending&page_size=1`); + Rest.get() + .then(({data}) => { + $rootScope.pendingApprovalCount = data.count; + }) + .catch(() => { + // TODO: handle this + }); }); // Call the API to get an auth token diff --git a/awx/ui/client/src/templates/templates.service.js b/awx/ui/client/src/templates/templates.service.js index f73c77b876..ef718064fd 100644 --- a/awx/ui/client/src/templates/templates.service.js +++ b/awx/ui/client/src/templates/templates.service.js @@ -75,221 +75,230 @@ export default ['Rest', 'GetBasePath', '$q', 'NextPage', function(Rest, GetBaseP }).catch(function(response){ return $q.reject( response ); }); - }, + }, - getAllWorkflowJobTemplateLabels: function(id) { - Rest.setUrl(GetBasePath('workflow_job_templates') + id + "/labels?page_size=200"); - return Rest.get() - .then(function(res) { - if (res.data.next) { - return NextPage({ - url: res.data.next, - arrayOfValues: res.data.results - }).then(function(labels) { - return labels; - }).catch(function(response){ - return $q.reject( response ); - }); - } - else { - return $q.resolve( res.data.results ); - } - }).catch(function(response){ - return $q.reject( response ); - }); - }, - getJobTemplate: function(id) { - var url = GetBasePath('job_templates'); + getAllWorkflowJobTemplateLabels: function(id) { + Rest.setUrl(GetBasePath('workflow_job_templates') + id + "/labels?page_size=200"); + return Rest.get() + .then(function(res) { + if (res.data.next) { + return NextPage({ + url: res.data.next, + arrayOfValues: res.data.results + }).then(function(labels) { + return labels; + }).catch(function(response){ + return $q.reject( response ); + }); + } + else { + return $q.resolve( res.data.results ); + } + }).catch(function(response){ + return $q.reject( response ); + }); + }, + getJobTemplate: function(id) { + var url = GetBasePath('job_templates'); - url = url + id; + url = url + id; - Rest.setUrl(url); - return Rest.get(); - }, - addWorkflowNode: function(params) { - // params.url - // params.data + Rest.setUrl(url); + return Rest.get(); + }, + addWorkflowNode: function(params) { + // params.url + // params.data - Rest.setUrl(params.url); - return Rest.post(params.data); - }, - editWorkflowNode: function(params) { - // params.id - // params.data + Rest.setUrl(params.url); + return Rest.post(params.data); + }, + editWorkflowNode: function(params) { + // params.id + // params.data - var url = GetBasePath('workflow_job_template_nodes') + params.id; + var url = GetBasePath('workflow_job_template_nodes') + params.id; - Rest.setUrl(url); - return Rest.put(params.data); - }, - getJobTemplateLaunchInfo: function(id) { - var url = GetBasePath('job_templates'); + Rest.setUrl(url); + return Rest.put(params.data); + }, + getJobTemplateLaunchInfo: function(id) { + var url = GetBasePath('job_templates'); - url = url + id + '/launch'; + url = url + id + '/launch'; - Rest.setUrl(url); - return Rest.get(); - }, - getWorkflowJobTemplateNodes: function(id, page) { - var url = GetBasePath('workflow_job_templates'); + Rest.setUrl(url); + return Rest.get(); + }, + getWorkflowJobTemplateNodes: function(id, page) { + var url = GetBasePath('workflow_job_templates'); - url = url + id + '/workflow_nodes?page_size=200'; + url = url + id + '/workflow_nodes?page_size=200'; - if(page) { - url += '&page=' + page; - } + if(page) { + url += '&page=' + page; + } - Rest.setUrl(url); - return Rest.get(); - }, - updateWorkflowJobTemplate: function(params) { - // params.id - // params.data + Rest.setUrl(url); + return Rest.get(); + }, + updateWorkflowJobTemplate: function(params) { + // params.id + // params.data - var url = GetBasePath('workflow_job_templates'); + var url = GetBasePath('workflow_job_templates'); - url = url + params.id; + url = url + params.id; - Rest.setUrl(url); - return Rest.patch(params.data); - }, - getWorkflowJobTemplate: function(id) { - var url = GetBasePath('workflow_job_templates'); + Rest.setUrl(url); + return Rest.patch(params.data); + }, + getWorkflowJobTemplate: function(id) { + var url = GetBasePath('workflow_job_templates'); - url = url + id; + url = url + id; - Rest.setUrl(url); - return Rest.get(); - }, - deleteWorkflowJobTemplateNode: function(id) { - var url = GetBasePath('workflow_job_template_nodes') + id; + Rest.setUrl(url); + return Rest.get(); + }, + deleteWorkflowJobTemplateNode: function(id) { + var url = GetBasePath('workflow_job_template_nodes') + id; - Rest.setUrl(url); - return Rest.destroy(); - }, - disassociateWorkflowNode: function(params) { - //params.parentId - //params.nodeId - //params.edge + Rest.setUrl(url); + return Rest.destroy(); + }, + disassociateWorkflowNode: function(params) { + //params.parentId + //params.nodeId + //params.edge - var url = GetBasePath('workflow_job_template_nodes') + params.parentId; + var url = GetBasePath('workflow_job_template_nodes') + params.parentId; - if(params.edge === 'success') { - url = url + '/success_nodes'; - } - else if(params.edge === 'failure') { - url = url + '/failure_nodes'; - } - else if(params.edge === 'always') { - url = url + '/always_nodes'; - } + if(params.edge === 'success') { + url = url + '/success_nodes'; + } + else if(params.edge === 'failure') { + url = url + '/failure_nodes'; + } + else if(params.edge === 'always') { + url = url + '/always_nodes'; + } - Rest.setUrl(url); - return Rest.post({ - "id": params.nodeId, - "disassociate": true - }); - }, - associateWorkflowNode: function(params) { - //params.parentId - //params.nodeId - //params.edge + Rest.setUrl(url); + return Rest.post({ + "id": params.nodeId, + "disassociate": true + }); + }, + associateWorkflowNode: function(params) { + //params.parentId + //params.nodeId + //params.edge - var url = GetBasePath('workflow_job_template_nodes') + params.parentId; + var url = GetBasePath('workflow_job_template_nodes') + params.parentId; - if(params.edge === 'success') { - url = url + '/success_nodes'; - } - else if(params.edge === 'failure') { - url = url + '/failure_nodes'; - } - else if(params.edge === 'always') { - url = url + '/always_nodes'; - } + if(params.edge === 'success') { + url = url + '/success_nodes'; + } + else if(params.edge === 'failure') { + url = url + '/failure_nodes'; + } + else if(params.edge === 'always') { + url = url + '/always_nodes'; + } - Rest.setUrl(url); - return Rest.post({ - id: params.nodeId - }); - }, - getUnifiedJobTemplate: function(id) { - var url = GetBasePath('unified_job_templates'); + Rest.setUrl(url); + return Rest.post({ + id: params.nodeId + }); + }, + getUnifiedJobTemplate: function(id) { + var url = GetBasePath('unified_job_templates'); - url = url + "?id=" + id; + url = url + "?id=" + id; - Rest.setUrl(url); - return Rest.get(); - }, - getCredential: function(id) { - var url = GetBasePath('credentials'); + Rest.setUrl(url); + return Rest.get(); + }, + getCredential: function(id) { + var url = GetBasePath('credentials'); - url = url + id; + url = url + id; - Rest.setUrl(url); - return Rest.get(); - }, - getInventory: function(id) { - var url = GetBasePath('inventory'); + Rest.setUrl(url); + return Rest.get(); + }, + getInventory: function(id) { + var url = GetBasePath('inventory'); - url = url + id; + url = url + id; - Rest.setUrl(url); - return Rest.get(); - }, - getWorkflowCopy: function(id) { - let url = GetBasePath('workflow_job_templates'); + Rest.setUrl(url); + return Rest.get(); + }, + getWorkflowCopy: function(id) { + let url = GetBasePath('workflow_job_templates'); - url = url + id + '/copy'; + url = url + id + '/copy'; - Rest.setUrl(url); - return Rest.get(); - }, - copyWorkflow: function(id) { - let url = GetBasePath('workflow_job_templates'); + Rest.setUrl(url); + return Rest.get(); + }, + copyWorkflow: function(id) { + let url = GetBasePath('workflow_job_templates'); - url = url + id + '/copy'; + url = url + id + '/copy'; - Rest.setUrl(url); - return Rest.post(); - }, - getWorkflowJobTemplateOptions: function() { - var deferred = $q.defer(); + Rest.setUrl(url); + return Rest.post(); + }, + getWorkflowJobTemplateOptions: function() { + var deferred = $q.defer(); - let url = GetBasePath('workflow_job_templates'); + let url = GetBasePath('workflow_job_templates'); - Rest.setUrl(url); - Rest.options() - .then(({data}) => { - deferred.resolve(data); - }).catch(({msg, code}) => { - deferred.reject(msg, code); - }); + Rest.setUrl(url); + Rest.options() + .then(({data}) => { + deferred.resolve(data); + }).catch(({msg, code}) => { + deferred.reject(msg, code); + }); - return deferred.promise; - }, - getJobTemplateOptions: function() { - var deferred = $q.defer(); + return deferred.promise; + }, + getJobTemplateOptions: function() { + var deferred = $q.defer(); - let url = GetBasePath('job_templates'); + let url = GetBasePath('job_templates'); - Rest.setUrl(url); - Rest.options() - .then(({data}) => { - deferred.resolve(data); - }).catch(({msg, code}) => { - deferred.reject(msg, code); - }); + Rest.setUrl(url); + Rest.options() + .then(({data}) => { + deferred.resolve(data); + }).catch(({msg, code}) => { + deferred.reject(msg, code); + }); - return deferred.promise; - }, - postWorkflowNodeCredential: function(params) { - // params.id - // params.data + return deferred.promise; + }, + postWorkflowNodeCredential: function(params) { + // params.id + // params.data - var url = GetBasePath('workflow_job_template_nodes') + params.id + '/credentials'; + var url = GetBasePath('workflow_job_template_nodes') + params.id + '/credentials'; - Rest.setUrl(url); - return Rest.post(params.data); - } + Rest.setUrl(url); + return Rest.post(params.data); + }, + createApprovalTemplate: (params) => { + params = params || {}; + Rest.setUrl(GetBasePath('workflow_approval_templates')); + return Rest.post(params); + }, + patchApprovalTemplate: ({id, data}) => { + Rest.setUrl(`${GetBasePath('workflow_approval_templates')}/${id}`); + return Rest.patch(data); + } }; }]; diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 830a02dcd1..c1719b07ac 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -142,8 +142,10 @@ } .WorkflowChart-deletedText { - width: 90px; + width: 180px; + height: 14px; color: @default-interface-txt; + text-align: center; } .WorkflowChart-activeNode { fill: @default-link; @@ -159,7 +161,11 @@ } .WorkflowChart-nameText { + width: 180px; + height: 20px; + line-height: 18px; font-size: 10px; + text-align: center; } .WorkflowChart-tooltip { diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index fb98fb05d0..8156407b5b 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -770,12 +770,23 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); baseSvg.selectAll(".WorkflowChart-nameText") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) + .attr("x", 0) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : (nodeH / 2) - 10; }) .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) - .text(function (d) { + .html(function (d) { const name = _.get(d, 'unifiedJobTemplate.name'); - return name ? wrap(name) : ""; + const wrappedName = name ? wrap(name) : ""; + // TODO: clean this up + if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type === 'workflow_approval') { + return ` +
+ +
+ ${wrappedName} +
`; + } else { + return `${wrappedName}`; + } }); baseSvg.selectAll(".WorkflowChart-detailsLink") @@ -884,19 +895,31 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("class", "WorkflowChart-activeNode") .style("display", function(d) { return d.id === scope.graphState.nodeBeingEdited ? null : "none"; }); - thisNode.append("text") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) + thisNode.append("foreignObject") + // .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) + .attr("x", 0) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : (nodeH / 2) - 10; }) .attr("dy", ".35em") .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .attr("class", "WorkflowChart-defaultText WorkflowChart-nameText") - .text(function (d) { + .html(function (d) { const name = _.get(d, 'unifiedJobTemplate.name'); - return name ? wrap(name) : ""; + const wrappedName = name ? wrap(name) : ""; + // TODO: clean this up + if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type === 'workflow_approval') { + return ` +
+ +
+ ${wrappedName} +
`; + } else { + return `${wrappedName}`; + } }); thisNode.append("foreignObject") - .attr("x", 62) + .attr("x", 0) .attr("y", 22) .attr("dy", ".35em") .attr("text-anchor", "middle") diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js index c089de6b7c..86c440482f 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js @@ -32,6 +32,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService $scope.strings = TemplatesStrings; $scope.editNodeHelpMessage = null; + $scope.pauseNode = {}; let templateList = _.cloneDeep(TemplateList); delete templateList.actions; @@ -463,6 +464,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } $scope.promptData = null; + $scope.pauseNode = {}; $scope.editNodeHelpMessage = getEditNodeHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj); if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") { @@ -616,26 +618,48 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService }) ); + CreateSelect2({ + element: '#workflow-node-types', + multiple: false + }); + $q.all(listPromises) .then(() => { if ($scope.nodeConfig.mode === "edit") { - // Make sure that we have the full unified job template object - if (!$scope.nodeConfig.node.fullUnifiedJobTemplateObject) { - // 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(({data}) => { - $scope.nodeConfig.node.fullUnifiedJobTemplateObject = data.results[0]; - finishConfiguringEdit(); - }, (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.node.unifiedJobTemplate && $scope.nodeConfig.node.unifiedJobTemplate.unified_job_type === "workflow_approval") { + $scope.selectedTemplate = null; + $scope.activeTab = "pause"; + CreateSelect2({ + element: '#workflow_node_edge', + multiple: false + }); + + $scope.pauseNode = { + isPauseNode: true, + name: $scope.nodeConfig.node.unifiedJobTemplate.name, + description: $scope.nodeConfig.node.unifiedJobTemplate.description, + }; + + $scope.nodeFormDataLoaded = true; } else { - finishConfiguringEdit(); + // Make sure that we have the full unified job template object + if (!$scope.nodeConfig.node.fullUnifiedJobTemplateObject) { + // 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(({data}) => { + $scope.nodeConfig.node.fullUnifiedJobTemplateObject = data.results[0]; + finishConfiguringEdit(); + }, (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 { finishConfiguringAdd(); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html index ed1a2f1470..089b24af5c 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html @@ -1,9 +1,18 @@
{{nodeConfig.mode === 'edit' ? nodeConfig.node.fullUnifiedJobTemplateObject.name || nodeConfig.node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_NODE')}}
-
-
{{strings.get('workflow_maker.JOBS')}}
-
{{strings.get('workflow_maker.PROJECT_SYNC')}}
-
{{strings.get('workflow_maker.INVENTORY_SYNC')}}
+
+
@@ -103,6 +112,36 @@
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
@@ -238,7 +277,8 @@ - + +
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index 760ceafc7d..87f20ce5db 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -296,8 +296,8 @@ border-bottom-left-radius: 5px; } -.WorkflowMaker-formTab { - margin-right: 10px; +.WorkflowMaker-formTypeDropdown { + margin-bottom: 20px; } .WorkflowMaker-preventBodyScrolling { @@ -314,6 +314,13 @@ margin-bottom: 20px; } +.WorkflowMaker-pauseCheckbox { + input { + margin-right: 5px; + } + margin-bottom: 20px; +} + .Key-list { margin: 0; padding: 20px; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index 68fcfda0b9..d5e166a7b5 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -140,59 +140,126 @@ export default ['$scope', 'TemplatesService', }; if ($scope.graphState.arrayOfNodesForChart.length > 1) { + let approvalTemplatePromises = []; let addPromises = []; let editPromises = []; let credentialRequests = []; Object.keys(nodeRef).map((workflowMakerNodeId) => { - if (nodeRef[workflowMakerNodeId].isNew) { - addPromises.push(TemplatesService.addWorkflowNode({ - url: $scope.workflowJobTemplateObj.related.workflow_nodes, - data: buildSendableNodeData(nodeRef[workflowMakerNodeId]) - }).then(({data}) => { - nodeRef[workflowMakerNodeId].originalNodeObject = data; - nodeIdToChartNodeIdMapping[data.id] = parseInt(workflowMakerNodeId); - if (_.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.ask_credential_on_launch')) { - // This finds the credentials that were selected in the prompt but don't occur - // in the template defaults - let credentialIdsToPost = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.value.filter((credFromPrompt) => { - let defaultCreds = _.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.defaults.credentials', []); - return !defaultCreds.some((defaultCred) => { - return credFromPrompt.id === defaultCred.id; + const node = nodeRef[workflowMakerNodeId]; + if (node.isNew) { + if (node.unifiedJobTemplate && node.unifiedJobTemplate.unified_job_type === "workflow_approval") { + approvalTemplatePromises.push(TemplatesService.createApprovalTemplate({ + name: node.unifiedJobTemplate.name + }).then(({data: approvalTemplateData}) => { + addPromises.push(TemplatesService.addWorkflowNode({ + url: $scope.workflowJobTemplateObj.related.workflow_nodes, + data: { + unified_job_template: approvalTemplateData.id + } + }).then(({data: nodeData}) => { + node.originalNodeObject = nodeData; + nodeIdToChartNodeIdMapping[nodeData.id] = parseInt(workflowMakerNodeId); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') }); + })); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') }); - - credentialIdsToPost.forEach((credentialToPost) => { - credentialRequests.push({ - id: data.id, + })); + } else { + addPromises.push(TemplatesService.addWorkflowNode({ + url: $scope.workflowJobTemplateObj.related.workflow_nodes, + data: buildSendableNodeData(node) + }).then(({data}) => { + node.originalNodeObject = data; + nodeIdToChartNodeIdMapping[data.id] = parseInt(workflowMakerNodeId); + if (_.get(node, 'promptData.launchConf.ask_credential_on_launch')) { + // This finds the credentials that were selected in the prompt but don't occur + // in the template defaults + let credentialIdsToPost = node.promptData.prompts.credentials.value.filter((credFromPrompt) => { + let defaultCreds = _.get(node, 'promptData.launchConf.defaults.credentials', []); + return !defaultCreds.some((defaultCred) => { + return credFromPrompt.id === defaultCred.id; + }); + }); + + credentialIdsToPost.forEach((credentialToPost) => { + credentialRequests.push({ + id: data.id, + data: { + id: credentialToPost.id + } + }); + }); + } + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + })); + } + } else if (node.isEdited) { + if (node.unifiedJobTemplate && node.unifiedJobTemplate.unified_job_type === "workflow_approval") { + if (node.originalNodeObject.summary_fields.unified_job_template.unified_job_type === "workflow_approval") { + approvalTemplatePromises.push(TemplatesService.patchApprovalTemplate({ + id: node.originalNodeObject.summary_fields.unified_job_template.id, + data: { + name: node.unifiedJobTemplate.name, + description: node.unifiedJobTemplate.description + } + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + })); + } else { + approvalTemplatePromises.push(TemplatesService.createApprovalTemplate({ + name: node.unifiedJobTemplate.name + }).then(({data: approvalTemplateData}) => { + // Make sure that this isn't overwriting everything on the node... + editPromises.push(TemplatesService.editWorkflowNode({ + url: $scope.workflowJobTemplateObj.related.workflow_nodes, data: { - id: credentialToPost.id + unified_job_template: approvalTemplateData.id } + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + })); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') }); - }); + })); } - }).catch(({ data, status }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') - }); - })); - } else if (nodeRef[workflowMakerNodeId].isEdited) { - editPromises.push(TemplatesService.editWorkflowNode({ - id: nodeRef[workflowMakerNodeId].originalNodeObject.id, - data: buildSendableNodeData(nodeRef[workflowMakerNodeId]) - })); + } else { + editPromises.push(TemplatesService.editWorkflowNode({ + id: node.originalNodeObject.id, + data: buildSendableNodeData(node) + })); + } - if (_.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.ask_credential_on_launch')) { - let credentialsNotInPriorCredentials = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.value.filter((credFromPrompt) => { - let defaultCreds = _.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.defaults.credentials', []); + if (_.get(node, 'promptData.launchConf.ask_credential_on_launch')) { + let credentialsNotInPriorCredentials = node.promptData.prompts.credentials.value.filter((credFromPrompt) => { + let defaultCreds = _.get(node, 'promptData.launchConf.defaults.credentials', []); return !defaultCreds.some((defaultCred) => { return credFromPrompt.id === defaultCred.id; }); }); let credentialsToAdd = credentialsNotInPriorCredentials.filter((credNotInPrior) => { - let previousOverrides = _.get(nodeRef[workflowMakerNodeId], 'promptData.prompts.credentials.previousOverrides', []); + let previousOverrides = _.get(node, 'promptData.prompts.credentials.previousOverrides', []); return !previousOverrides.some((priorCred) => { return credNotInPrior.id === priorCred.id; }); @@ -200,8 +267,8 @@ export default ['$scope', 'TemplatesService', let credentialsToRemove = []; - if (_.has(nodeRef[workflowMakerNodeId], 'promptData.prompts.credentials.previousOverrides')) { - credentialsToRemove = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.previousOverrides.filter((priorCred) => { + if (_.has(node, 'promptData.prompts.credentials.previousOverrides')) { + credentialsToRemove = node.promptData.prompts.credentials.previousOverrides.filter((priorCred) => { return !credentialsNotInPriorCredentials.some((credNotInPrior) => { return priorCred.id === credNotInPrior.id; }); @@ -210,7 +277,7 @@ export default ['$scope', 'TemplatesService', credentialsToAdd.forEach((credentialToAdd) => { credentialRequests.push({ - id: nodeRef[workflowMakerNodeId].originalNodeObject.id, + id: node.originalNodeObject.id, data: { id: credentialToAdd.id } @@ -219,7 +286,7 @@ export default ['$scope', 'TemplatesService', credentialsToRemove.forEach((credentialToRemove) => { credentialRequests.push({ - id: nodeRef[workflowMakerNodeId].originalNodeObject.id, + id: node.originalNodeObject.id, data: { id: credentialToRemove.id, disassociate: true @@ -235,172 +302,177 @@ export default ['$scope', 'TemplatesService', return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); }); - $q.all(addPromises.concat(editPromises, deletePromises)) + $q.all(approvalTemplatePromises) .then(() => { - let disassociatePromises = []; - let associatePromises = []; - let linkMap = {}; - - // Build a link map for easy access - $scope.graphState.arrayOfLinksForChart.forEach(link => { - // link.source.id of 1 is our artificial start node - if (link.source.id !== 1) { - const sourceNodeId = nodeRef[link.source.id].originalNodeObject.id; - const targetNodeId = nodeRef[link.target.id].originalNodeObject.id; - if (!linkMap[sourceNodeId]) { - linkMap[sourceNodeId] = {}; + $q.all(addPromises.concat(editPromises, deletePromises)) + .then(() => { + let disassociatePromises = []; + let associatePromises = []; + let linkMap = {}; + + // Build a link map for easy access + $scope.graphState.arrayOfLinksForChart.forEach(link => { + // link.source.id of 1 is our artificial start node + if (link.source.id !== 1) { + const sourceNodeId = nodeRef[link.source.id].originalNodeObject.id; + const targetNodeId = nodeRef[link.target.id].originalNodeObject.id; + if (!linkMap[sourceNodeId]) { + linkMap[sourceNodeId] = {}; + } + + linkMap[sourceNodeId][targetNodeId] = link.edgeType; } - - linkMap[sourceNodeId][targetNodeId] = link.edgeType; - } - }); - - Object.keys(nodeRef).map((workflowNodeId) => { - let nodeId = nodeRef[workflowNodeId].originalNodeObject.id; - if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) { - nodeRef[workflowNodeId].originalNodeObject.success_nodes.forEach((successNodeId) => { - if ( - !deletedNodeIds.includes(successNodeId) && - (!linkMap[nodeId] || - !linkMap[nodeId][successNodeId] || - linkMap[nodeId][successNodeId] !== "success") - ) { - disassociatePromises.push( - TemplatesService.disassociateWorkflowNode({ - parentId: nodeId, - nodeId: successNodeId, - edge: "success" - }) - ); - } - }); - } - if (nodeRef[workflowNodeId].originalNodeObject.failure_nodes) { - nodeRef[workflowNodeId].originalNodeObject.failure_nodes.forEach((failureNodeId) => { - if ( - !deletedNodeIds.includes(failureNodeId) && - (!linkMap[nodeId] || - !linkMap[nodeId][failureNodeId] || - linkMap[nodeId][failureNodeId] !== "failure") - ) { - disassociatePromises.push( - TemplatesService.disassociateWorkflowNode({ - parentId: nodeId, - nodeId: failureNodeId, - edge: "failure" - }) - ); - } - }); - } - if (nodeRef[workflowNodeId].originalNodeObject.always_nodes) { - nodeRef[workflowNodeId].originalNodeObject.always_nodes.forEach((alwaysNodeId) => { - if ( - !deletedNodeIds.includes(alwaysNodeId) && - (!linkMap[nodeId] || - !linkMap[nodeId][alwaysNodeId] || - linkMap[nodeId][alwaysNodeId] !== "always") - ) { - disassociatePromises.push( - TemplatesService.disassociateWorkflowNode({ - parentId: nodeId, - nodeId: alwaysNodeId, - edge: "always" - }) - ); - } - }); - } - }); - - Object.keys(linkMap).map((sourceNodeId) => { - Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => { - const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId]; - const targetChartNodeId = nodeIdToChartNodeIdMapping[targetNodeId]; - switch(linkMap[sourceNodeId][targetNodeId]) { - case "success": + }); + + Object.keys(nodeRef).map((workflowNodeId) => { + let nodeId = nodeRef[workflowNodeId].originalNodeObject.id; + if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) { + nodeRef[workflowNodeId].originalNodeObject.success_nodes.forEach((successNodeId) => { if ( - !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes || - !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + !deletedNodeIds.includes(successNodeId) && + (!linkMap[nodeId] || + !linkMap[nodeId][successNodeId] || + linkMap[nodeId][successNodeId] !== "success") ) { - associatePromises.push( - TemplatesService.associateWorkflowNode({ - parentId: parseInt(sourceNodeId), - nodeId: parseInt(targetNodeId), + disassociatePromises.push( + TemplatesService.disassociateWorkflowNode({ + parentId: nodeId, + nodeId: successNodeId, edge: "success" }) ); } - break; - case "failure": + }); + } + if (nodeRef[workflowNodeId].originalNodeObject.failure_nodes) { + nodeRef[workflowNodeId].originalNodeObject.failure_nodes.forEach((failureNodeId) => { if ( - !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes || - !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + !deletedNodeIds.includes(failureNodeId) && + (!linkMap[nodeId] || + !linkMap[nodeId][failureNodeId] || + linkMap[nodeId][failureNodeId] !== "failure") ) { - associatePromises.push( - TemplatesService.associateWorkflowNode({ - parentId: parseInt(sourceNodeId), - nodeId: parseInt(targetNodeId), + disassociatePromises.push( + TemplatesService.disassociateWorkflowNode({ + parentId: nodeId, + nodeId: failureNodeId, edge: "failure" }) ); } - break; - case "always": + }); + } + if (nodeRef[workflowNodeId].originalNodeObject.always_nodes) { + nodeRef[workflowNodeId].originalNodeObject.always_nodes.forEach((alwaysNodeId) => { if ( - !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes || - !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + !deletedNodeIds.includes(alwaysNodeId) && + (!linkMap[nodeId] || + !linkMap[nodeId][alwaysNodeId] || + linkMap[nodeId][alwaysNodeId] !== "always") ) { - associatePromises.push( - TemplatesService.associateWorkflowNode({ - parentId: parseInt(sourceNodeId), - nodeId: parseInt(targetNodeId), + disassociatePromises.push( + TemplatesService.disassociateWorkflowNode({ + parentId: nodeId, + nodeId: alwaysNodeId, edge: "always" }) ); } - break; + }); } }); - }); - - $q.all(disassociatePromises) - .then(() => { - let credentialPromises = credentialRequests.map((request) => { - return TemplatesService.postWorkflowNodeCredential({ - id: request.id, - data: request.data - }); - }); - - return $q.all(associatePromises.concat(credentialPromises)) - .then(() => { - Wait('stop'); - $scope.workflowChangesUnsaved = false; - $scope.workflowChangesStarted = false; - $scope.closeDialog(); - }).catch(({ data, status }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') - }); - }); - }).catch(({ - data, - status - }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') + + Object.keys(linkMap).map((sourceNodeId) => { + Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => { + const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId]; + const targetChartNodeId = nodeIdToChartNodeIdMapping[targetNodeId]; + switch(linkMap[sourceNodeId][targetNodeId]) { + case "success": + if ( + !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes || + !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + ) { + associatePromises.push( + TemplatesService.associateWorkflowNode({ + parentId: parseInt(sourceNodeId), + nodeId: parseInt(targetNodeId), + edge: "success" + }) + ); + } + break; + case "failure": + if ( + !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes || + !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + ) { + associatePromises.push( + TemplatesService.associateWorkflowNode({ + parentId: parseInt(sourceNodeId), + nodeId: parseInt(targetNodeId), + edge: "failure" + }) + ); + } + break; + case "always": + if ( + !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes || + !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + ) { + associatePromises.push( + TemplatesService.associateWorkflowNode({ + parentId: parseInt(sourceNodeId), + nodeId: parseInt(targetNodeId), + edge: "always" + }) + ); + } + break; + } }); }); - }).catch(({ data, status }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') + + $q.all(disassociatePromises) + .then(() => { + let credentialPromises = credentialRequests.map((request) => { + return TemplatesService.postWorkflowNodeCredential({ + id: request.id, + data: request.data + }); + }); + + return $q.all(associatePromises.concat(credentialPromises)) + .then(() => { + Wait('stop'); + $scope.workflowChangesUnsaved = false; + $scope.workflowChangesStarted = false; + $scope.closeDialog(); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + }); + }).catch(({ + data, + status + }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + }); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); }); + }) + .catch(() => { + // TODO: handle }); - } else { let deletePromises = deletedNodeIds.map((nodeId) => { @@ -511,17 +583,27 @@ export default ['$scope', 'TemplatesService', $scope.formState.showNodeForm = true; }; - $scope.confirmNodeForm = (selectedTemplate, promptData, edgeType) => { + $scope.confirmNodeForm = (selectedTemplate, promptData, edgeType, pauseNode) => { $scope.workflowChangesUnsaved = true; const nodeId = $scope.nodeConfig.nodeId; if ($scope.nodeConfig.mode === "add") { - if (selectedTemplate && edgeType && edgeType.value) { - nodeRef[$scope.nodeConfig.nodeId] = { - fullUnifiedJobTemplateObject: selectedTemplate, - promptData, - isNew: true - }; - + if (edgeType && edgeType.value) { + if (selectedTemplate) { + nodeRef[$scope.nodeConfig.nodeId] = { + fullUnifiedJobTemplateObject: selectedTemplate, + promptData, + isNew: true + }; + } else if (pauseNode && pauseNode.isPauseNode) { + nodeRef[$scope.nodeConfig.nodeId] = { + unifiedJobTemplate: { + name: pauseNode.name, + description: pauseNode.description, + unified_job_type: "workflow_approval" + }, + isNew: true + }; + } $scope.graphState.nodeBeingAdded = null; $scope.graphState.arrayOfLinksForChart.map( (link) => { @@ -534,6 +616,7 @@ export default ['$scope', 'TemplatesService', } else if ($scope.nodeConfig.mode === "edit") { if (selectedTemplate) { nodeRef[$scope.nodeConfig.nodeId].fullUnifiedJobTemplateObject = selectedTemplate; + nodeRef[$scope.nodeConfig.nodeId].unifiedJobTemplate = selectedTemplate; nodeRef[$scope.nodeConfig.nodeId].promptData = _.cloneDeep(promptData); nodeRef[$scope.nodeConfig.nodeId].isEdited = true; $scope.graphState.nodeBeingEdited = null; @@ -546,12 +629,30 @@ export default ['$scope', 'TemplatesService', link.source.unifiedJobTemplate = selectedTemplate; } }); + } else if (pauseNode && pauseNode.isPauseNode) { + // If it's a _new_ pause node then we'll want to create the new ujt + // If it's an existing pause node then we'll want to update the ujt + nodeRef[$scope.nodeConfig.nodeId].unifiedJobTemplate = { + name: pauseNode.name, + description: pauseNode.description, + unified_job_type: "workflow_approval" + }, + nodeRef[$scope.nodeConfig.nodeId].isEdited = true; } } $scope.graphState.arrayOfNodesForChart.map( (node) => { if (node.id === nodeId) { - node.unifiedJobTemplate = selectedTemplate; + if (pauseNode && pauseNode.isPauseNode) { + node.unifiedJobTemplate = { + unified_job_type: 'workflow_approval', + name: pauseNode.name, + description: pauseNode.description + }; + } else { + node.unifiedJobTemplate = selectedTemplate; + } + } }); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index 4e986586ea..d8685582fe 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -128,7 +128,7 @@
- +