diff --git a/awx/ui/client/features/templates/list-templates.controller.js b/awx/ui/client/features/templates/list-templates.controller.js index f6f9123272..321c991011 100644 --- a/awx/ui/client/features/templates/list-templates.controller.js +++ b/awx/ui/client/features/templates/list-templates.controller.js @@ -16,14 +16,11 @@ function ListTemplatesController( $state, Alert, Dataset, - InitiatePlaybookRun, ProcessErrors, Prompt, - PromptService, resolvedModels, strings, - Wait, - Empty + Wait ) { const vm = this || {}; const [jobTemplate, workflowTemplate] = resolvedModels; @@ -42,7 +39,7 @@ function ListTemplatesController( position: 'right', arrowHeight: 15 } - } + }; $scope.canAddJobTemplate = jobTemplate.options('actions.POST'); $scope.canAddWorkflowJobTemplate = workflowTemplate.options('actions.POST'); @@ -70,21 +67,6 @@ function ListTemplatesController( } }; - vm.runTemplate = template => { - if (!template) { - Alert(strings.get('error.LAUNCH'), strings.get('alert.MISSING_PARAMETER')); - return; - } - - if (isJobTemplate(template)) { - runJobTemplate(template); - } else if (isWorkflowTemplate(template)) { - runWorkflowTemplate(template); - } else { - Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_LAUNCH')); - } - }; - vm.scheduleTemplate = template => { if (!template) { Alert(strings.get('error.SCHEDULE'), strings.get('alert.MISSING_PARAMETER')); @@ -325,123 +307,6 @@ function ListTemplatesController( return html; } - - function runJobTemplate(template) { - const selectedJobTemplate = jobTemplate.create(); - const preLaunchPromises = [ - selectedJobTemplate.getLaunch(template.id), - selectedJobTemplate.optionsLaunch(template.id), - ]; - - Promise.all(preLaunchPromises) - .then(([launchData, launchOptions]) => { - if (selectedJobTemplate.canLaunchWithoutPrompt()) { - return selectedJobTemplate - .postLaunch({ id: template.id }) - .then(({ data }) => { - $state.go('jobResult', { id: data.job }, { reload: true }); - }); - } - - const promptData = { - launchConf: launchData.data, - launchOptions: launchOptions.data, - template: template.id, - prompts: PromptService.processPromptValues({ - launchConf: launchData.data, - launchOptions: launchOptions.data - }), - triggerModalOpen: true, - }; - - if (launchData.data.survey_enabled) { - selectedJobTemplate.getSurveyQuestions(template.id) - .then(({ data }) => { - const processed = PromptService.processSurveyQuestions({ surveyQuestions: data.spec }); - promptData.surveyQuestions = processed.surveyQuestions; - $scope.promptData = promptData; - }); - } else { - $scope.promptData = promptData; - } - }); - } - - function runWorkflowTemplate(template) { - InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' }); - } - - $scope.launchJob = () => { - const jobLaunchData = { - extra_vars: $scope.promptData.extraVars - }; - - if ($scope.promptData.launchConf.ask_tags_on_launch){ - jobLaunchData.job_tags = $scope.promptData.prompts.tags.value.map(a => a.value).join(); - } - - if ($scope.promptData.launchConf.ask_skip_tags_on_launch){ - jobLaunchData.skip_tags = $scope.promptData.prompts.skipTags.value.map(a => a.value).join(); - } - - if ($scope.promptData.launchConf.ask_limit_on_launch && _.has($scope, 'promptData.prompts.limit.value')){ - jobLaunchData.limit = $scope.promptData.prompts.limit.value; - } - - if ($scope.promptData.launchConf.ask_job_type_on_launch && _.has($scope, 'promptData.prompts.jobType.value.value')) { - jobLaunchData.job_type = $scope.promptData.prompts.jobType.value.value; - } - - if ($scope.promptData.launchConf.ask_verbosity_on_launch && _.has($scope, 'promptData.prompts.verbosity.value.value')) { - jobLaunchData.verbosity = $scope.promptData.prompts.verbosity.value.value; - } - - if ($scope.promptData.launchConf.ask_inventory_on_launch && !Empty($scope.promptData.prompts.inventory.value.id)){ - jobLaunchData.inventory_id = $scope.promptData.prompts.inventory.value.id; - } - - if ($scope.promptData.launchConf.ask_credential_on_launch){ - jobLaunchData.credentials = []; - $scope.promptData.prompts.credentials.value.forEach((credential) => { - jobLaunchData.credentials.push(credential.id); - }); - } - - if ($scope.promptData.launchConf.ask_diff_mode_on_launch && _.has($scope, 'promptData.prompts.diffMode.value')) { - jobLaunchData.diff_mode = $scope.promptData.prompts.diffMode.value; - } - - if ($scope.promptData.prompts.credentials.passwords) { - _.forOwn($scope.promptData.prompts.credentials.passwords, (val, key) => { - if (!jobLaunchData.credential_passwords) { - jobLaunchData.credential_passwords = {}; - } - if (key === "ssh_key_unlock") { - jobLaunchData.credential_passwords.ssh_key_unlock = val.value; - } else if (key !== "vault") { - jobLaunchData.credential_passwords[`${key}_password`] = val.value; - } else { - _.each(val, (vaultCred) => { - jobLaunchData.credential_passwords[vaultCred.vault_id ? `${key}_password.${vaultCred.vault_id}` : `${key}_password`] = vaultCred.value; - }); - } - }); - } - - // If the extra_vars dict is empty, we don't want to include it if we didn't prompt for anything. - if(_.isEmpty(jobLaunchData.extra_vars) && !($scope.promptData.launchConf.ask_variables_on_launch && $scope.promptData.launchConf.survey_enabled && $scope.promptData.surveyQuestions.length > 0)){ - delete jobLaunchData.extra_vars; - } - - jobTemplate.create().postLaunch({ - id: $scope.promptData.template, - launchData: jobLaunchData - }) - .then((launchRes) => { - $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); - }) - .catch(createErrorHandler('launch job template', 'POST')); - }; } ListTemplatesController.$inject = [ @@ -450,14 +315,11 @@ ListTemplatesController.$inject = [ '$state', 'Alert', 'Dataset', - 'InitiatePlaybookRun', 'ProcessErrors', 'Prompt', - 'PromptService', 'resolvedModels', 'TemplatesStrings', - 'Wait', - 'Empty' + 'Wait' ]; export default ListTemplatesController; diff --git a/awx/ui/client/features/templates/list.route.js b/awx/ui/client/features/templates/list.route.js index 2750a0e8a7..e08b2fc863 100644 --- a/awx/ui/client/features/templates/list.route.js +++ b/awx/ui/client/features/templates/list.route.js @@ -27,7 +27,7 @@ export default { dynamic: true, value: { type: 'workflow_job_template,job_template', - }, + }, } }, searchPrefix: 'template', @@ -61,7 +61,7 @@ export default { Wait('start'); return qs.search(searchPath, searchParam) - .finally(() => Wait('stop')) + .finally(() => Wait('stop')); } ], } diff --git a/awx/ui/client/features/templates/list.view.html b/awx/ui/client/features/templates/list.view.html index d91e5f281e..a6557423ce 100644 --- a/awx/ui/client/features/templates/list.view.html +++ b/awx/ui/client/features/templates/list.view.html @@ -98,9 +98,9 @@
- - + @@ -121,5 +121,4 @@ query-set="querySet"> - diff --git a/awx/ui/client/lib/components/_index.less b/awx/ui/client/lib/components/_index.less index 6fdef9ece1..ae992079c9 100644 --- a/awx/ui/client/lib/components/_index.less +++ b/awx/ui/client/lib/components/_index.less @@ -1,11 +1,12 @@ @import 'action/_index'; @import 'input/_index'; +@import 'launchTemplateButton/_index'; @import 'layout/_index'; @import 'list/_index'; @import 'modal/_index'; @import 'panel/_index'; @import 'popover/_index'; +@import 'relaunchButton/_index'; @import 'tabs/_index'; -@import 'utility/_index'; @import 'truncate/_index'; -@import 'relaunchButton/_index'; \ No newline at end of file +@import 'utility/_index'; diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index d33e79329a..9ac933628c 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -16,23 +16,24 @@ import inputSlider from '~components/input/slider.directive'; import inputText from '~components/input/text.directive'; import inputTextarea from '~components/input/textarea.directive'; import inputTextareaSecret from '~components/input/textarea-secret.directive'; +import launchTemplate from '~components/launchTemplateButton/launchTemplateButton.component'; import layout from '~components/layout/layout.directive'; import list from '~components/list/list.directive'; -import row from '~components/list/row.directive'; -import rowItem from '~components/list/row-item.directive'; -import rowAction from '~components/list/row-action.directive'; import modal from '~components/modal/modal.directive'; import panel from '~components/panel/panel.directive'; import panelBody from '~components/panel/body.directive'; import panelHeading from '~components/panel/heading.directive'; import popover from '~components/popover/popover.directive'; +import relaunch from '~components/relaunchButton/relaunchButton.component'; +import row from '~components/list/row.directive'; +import rowItem from '~components/list/row-item.directive'; +import rowAction from '~components/list/row-action.directive'; import sideNav from '~components/layout/side-nav.directive'; import sideNavItem from '~components/layout/side-nav-item.directive'; import tab from '~components/tabs/tab.directive'; import tabGroup from '~components/tabs/group.directive'; import topNavItem from '~components/layout/top-nav-item.directive'; import truncate from '~components/truncate/truncate.directive'; -import relaunch from '~components/relaunchButton/relaunchButton.component'; import BaseInputController from '~components/input/base.controller'; import ComponentsStrings from '~components/components.strings'; @@ -59,8 +60,10 @@ angular .directive('atInputText', inputText) .directive('atInputTextarea', inputTextarea) .directive('atInputTextareaSecret', inputTextareaSecret) + .component('atLaunchTemplate', launchTemplate) .directive('atLayout', layout) .directive('atList', list) + .component('atRelaunch', relaunch) .directive('atRow', row) .directive('atRowItem', rowItem) .directive('atRowAction', rowAction) @@ -75,7 +78,6 @@ angular .directive('atTabGroup', tabGroup) .directive('atTopNavItem', topNavItem) .directive('atTruncate', truncate) - .component('atRelaunch', relaunch) .service('BaseInputController', BaseInputController) .service('ComponentsStrings', ComponentsStrings); diff --git a/awx/ui/client/lib/components/launchTemplateButton/_index.less b/awx/ui/client/lib/components/launchTemplateButton/_index.less new file mode 100644 index 0000000000..d5c39547ff --- /dev/null +++ b/awx/ui/client/lib/components/launchTemplateButton/_index.less @@ -0,0 +1,24 @@ +.at-LaunchTemplate { + margin-left: 15px; + + &--button { + font-size: 16px; + height: 30px; + min-width: 30px; + color: #848992; + background-color: inherit; + border: none; + border-radius: 4px; + } + &--button:hover { + background-color: @at-blue; + color: white; + } +} + +.open { + .at-LaunchTemplate--button { + background-color: @at-blue; + color: white; + } +} diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js new file mode 100644 index 0000000000..4081f29bff --- /dev/null +++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js @@ -0,0 +1,152 @@ +import templateUrl from './launchTemplateButton.partial.html'; + +const atLaunchTemplate = { + templateUrl, + bindings: { + template: '<' + }, + controller: ['JobTemplateModel', 'WorkflowJobTemplateModel', 'PromptService', '$state', + 'ProcessErrors', '$scope', 'TemplatesStrings', 'Alert', atLaunchTemplateCtrl], + controllerAs: 'vm' +}; + +function atLaunchTemplateCtrl ( + JobTemplate, WorkflowTemplate, PromptService, $state, + ProcessErrors, $scope, strings, Alert +) { + const vm = this; + const jobTemplate = new JobTemplate(); + const workflowTemplate = new WorkflowTemplate(); + + const createErrorHandler = (path, action) => + ({ data, status }) => { + const hdr = strings.get('error.HEADER'); + const msg = strings.get('error.CALL', { path, action, status }); + ProcessErrors($scope, data, status, null, { hdr, msg }); + }; + + vm.startLaunchTemplate = () => { + if (vm.template.type === 'job_template') { + const selectedJobTemplate = jobTemplate.create(); + const preLaunchPromises = [ + selectedJobTemplate.getLaunch(vm.template.id), + selectedJobTemplate.optionsLaunch(vm.template.id), + ]; + + Promise.all(preLaunchPromises) + .then(([launchData, launchOptions]) => { + if (selectedJobTemplate.canLaunchWithoutPrompt()) { + selectedJobTemplate + .postLaunch({ id: vm.template.id }) + .then(({ data }) => { + $state.go('jobResult', { id: data.job }, { reload: true }); + }); + } else { + const promptData = { + launchConf: launchData.data, + launchOptions: launchOptions.data, + template: vm.template.id, + templateType: vm.template.type, + prompts: PromptService.processPromptValues({ + launchConf: launchData.data, + launchOptions: launchOptions.data + }), + triggerModalOpen: true, + }; + + if (launchData.data.survey_enabled) { + selectedJobTemplate.getSurveyQuestions(vm.template.id) + .then(({ data }) => { + const processed = PromptService.processSurveyQuestions({ + surveyQuestions: data.spec + }); + promptData.surveyQuestions = processed.surveyQuestions; + vm.promptData = promptData; + }); + } else { + vm.promptData = promptData; + } + } + }); + } else if (vm.template.type === 'workflow_job_template') { + const selectedWorkflowJobTemplate = workflowTemplate.create(); + const preLaunchPromises = [ + selectedWorkflowJobTemplate.getLaunch(vm.template.id), + selectedWorkflowJobTemplate.optionsLaunch(vm.template.id), + ]; + + Promise.all(preLaunchPromises) + .then(([launchData, launchOptions]) => { + if (selectedWorkflowJobTemplate.canLaunchWithoutPrompt()) { + selectedWorkflowJobTemplate + .postLaunch({ id: vm.template.id }) + .then(({ data }) => { + $state.go('workflowResults', { id: data.workflow_job }, { reload: true }); + }); + } else { + const promptData = { + launchConf: launchData.data, + launchOptions: launchOptions.data, + template: vm.template.id, + templateType: vm.template.type, + prompts: PromptService.processPromptValues({ + launchConf: launchData.data, + launchOptions: launchOptions.data + }), + triggerModalOpen: true, + }; + + if (launchData.data.survey_enabled) { + selectedWorkflowJobTemplate.getSurveyQuestions(vm.template.id) + .then(({ data }) => { + const processed = PromptService.processSurveyQuestions({ + surveyQuestions: data.spec + }); + promptData.surveyQuestions = processed.surveyQuestions; + vm.promptData = promptData; + }); + } else { + vm.promptData = promptData; + } + } + }); + } else { + Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_LAUNCH')); + } + }; + + vm.launchTemplateWithPrompts = () => { + const jobLaunchData = PromptService.bundlePromptDataForLaunch(vm.promptData); + + // If the extra_vars dict is empty, we don't want to include it + // if we didn't prompt for anything. + if ( + _.isEmpty(jobLaunchData.extra_vars) && + !( + vm.promptData.launchConf.ask_variables_on_launch && + vm.promptData.launchConf.survey_enabled && + vm.promptData.surveyQuestions.length > 0 + ) + ) { + delete jobLaunchData.extra_vars; + } + + if (vm.promptData.templateType === 'job_template') { + jobTemplate.create().postLaunch({ + id: vm.promptData.template, + launchData: jobLaunchData + }).then((launchRes) => { + $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); + }).catch(createErrorHandler('launch job template', 'POST')); + } else if (vm.promptData.templateType === 'workflow_job_template') { + workflowTemplate.create().postLaunch({ + id: vm.promptData.template, + launchData: jobLaunchData + }).then((launchRes) => { + $state.go('workflowResults', { id: launchRes.data.workflow_job }, { reload: true }); + }).catch(createErrorHandler('launch workflow job template', 'POST')); + } + }; +} + +export default atLaunchTemplate; diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html new file mode 100644 index 0000000000..5be2373264 --- /dev/null +++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html @@ -0,0 +1,7 @@ +
+ + +
diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js index 465404c10e..045afd3585 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js @@ -3,20 +3,118 @@ import templateUrl from './relaunchButton.partial.html'; const atRelaunch = { templateUrl, bindings: { - state: '<' + job: '<' }, - controller: ['RelaunchJob', 'InitiatePlaybookRun', 'ComponentsStrings', '$scope', atRelaunchCtrl], + controller: ['ProcessErrors', 'AdhocRun', 'ComponentsStrings', + 'ProjectModel', 'InventorySourceModel', 'WorkflowJobModel', 'Alert', + 'AdHocCommandModel', 'JobModel', 'JobTemplateModel', 'PromptService', + '$state', '$q', '$scope', atRelaunchCtrl + ], controllerAs: 'vm' }; -function atRelaunchCtrl (RelaunchJob, InitiatePlaybookRun, strings, $scope) { +function atRelaunchCtrl ( + ProcessErrors, AdhocRun, strings, + Project, InventorySource, WorkflowJob, Alert, + AdHocCommand, Job, JobTemplate, PromptService, + $state, $q, $scope +) { const vm = this; - const scope = $scope.$parent; - const job = _.get(scope, 'job') || _.get(scope, 'completed_job'); + const jobObj = new Job(); + const jobTemplate = new JobTemplate(); + + const checkRelaunchPlaybook = (option) => { + jobObj.getRelaunch({ + id: vm.job.id + }).then((getRelaunchRes) => { + if ( + getRelaunchRes.data.passwords_needed_to_start && + getRelaunchRes.data.passwords_needed_to_start.length > 0 + ) { + const jobPromises = [ + jobObj.request('get', vm.job.id), + jobTemplate.optionsLaunch(vm.job.unified_job_template) + ]; + + $q.all(jobPromises) + .then(([jobRes, launchOptions]) => { + const populatedJob = jobRes.data; + const jobTypeChoices = _.get( + launchOptions, + 'data.actions.POST.job_type.choices', + [] + ).map(c => ({ label: c[1], value: c[0] })); + const verbosityChoices = _.get( + launchOptions, + 'data.actions.POST.verbosity.choices', + [] + ).map(c => ({ label: c[1], value: c[0] })); + const verbosity = _.find( + verbosityChoices, + item => item.value === populatedJob.verbosity + ); + const jobType = _.find( + jobTypeChoices, + item => item.value === populatedJob.job_type + ); + + vm.promptData = { + launchConf: { + passwords_needed_to_start: + getRelaunchRes.data.passwords_needed_to_start + }, + launchOptions: launchOptions.data, + job: vm.job.id, + relaunchHostType: option ? (option.name).toLowerCase() : null, + prompts: { + credentials: { + value: populatedJob.summary_fields.credentials || [] + }, + variables: { + value: populatedJob.extra_vars + }, + inventory: { + value: populatedJob.summary_fields.inventory || null + }, + verbosity: { + value: verbosity, + choices: verbosityChoices + }, + jobType: { + value: jobType, + choices: jobTypeChoices + }, + limit: { + value: populatedJob.limit + }, + tags: { + value: populatedJob.job_tags + }, + skipTags: { + value: populatedJob.skip_tags + }, + diffMode: { + value: populatedJob.diff_mode + } + }, + triggerModalOpen: true + }; + }); + } else { + jobObj.postRelaunch({ + id: vm.job.id + }).then((launchRes) => { + if (!$state.includes('jobs')) { + $state.go('jobResult', { id: launchRes.data.id }, { reload: true }); + } + }); + } + }); + }; vm.$onInit = () => { - vm.showRelaunch = !(job.type === 'system_job') && job.summary_fields.user_capabilities.start; - vm.showDropdown = job.type === 'job' && job.failed === true; + vm.showRelaunch = vm.job.type !== 'system_job' && vm.job.summary_fields.user_capabilities.start; + vm.showDropdown = vm.job.type === 'job' && vm.job.failed === true; vm.createDropdown(); vm.createTooltips(); @@ -46,27 +144,97 @@ function atRelaunchCtrl (RelaunchJob, InitiatePlaybookRun, strings, $scope) { }; vm.relaunchJob = () => { - let typeId; + if (vm.job.type === 'inventory_update') { + const inventorySource = new InventorySource(); - if (job.type === 'inventory_update') { - typeId = job.inventory_source; - } else if (job.type === 'project_update') { - typeId = job.project; - } else if (job.type === 'job' || job.type === 'system_job' - || job.type === 'ad_hoc_command' || job.type === 'workflow_job') { - typeId = job.id; + inventorySource.getUpdate(vm.job.inventory_source) + .then((getUpdateRes) => { + if (getUpdateRes.data.can_update) { + inventorySource.postUpdate(vm.job.inventory_source) + .then((postUpdateRes) => { + if (!$state.includes('jobs')) { + $state.go('inventorySyncStdout', { id: postUpdateRes.data.id }, { reload: true }); + } + }); + } else { + Alert( + 'Permission Denied', 'You do not have permission to sync this inventory source. Please contact your system administrator.', + 'alert-danger' + ); + } + }); + } else if (vm.job.type === 'project_update') { + const project = new Project(); + + project.getUpdate(vm.job.project) + .then((getUpdateRes) => { + if (getUpdateRes.data.can_update) { + project.postUpdate(vm.job.project) + .then((postUpdateRes) => { + if (!$state.includes('jobs')) { + $state.go('scmUpdateStdout', { id: postUpdateRes.data.id }, { reload: true }); + } + }); + } else { + Alert( + 'Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', + 'alert-danger' + ); + } + }); + } else if (vm.job.type === 'workflow_job') { + const workflowJob = new WorkflowJob(); + + workflowJob.postRelaunch({ + id: vm.job.id + }).then((launchRes) => { + if (!$state.includes('jobs')) { + $state.go('workflowResults', { id: launchRes.data.id }, { reload: true }); + } + }); + } else if (vm.job.type === 'ad_hoc_command') { + const adHocCommand = new AdHocCommand(); + + adHocCommand.getRelaunch({ + id: vm.job.id + }).then((getRelaunchRes) => { + if ( + getRelaunchRes.data.passwords_needed_to_start && + getRelaunchRes.data.passwords_needed_to_start.length > 0 + ) { + AdhocRun({ scope: $scope, project_id: vm.job.id, relaunch: true }); + } else { + adHocCommand.postRelaunch({ + id: vm.job.id + }).then((launchRes) => { + if (!$state.includes('jobs')) { + $state.go('adHocJobStdout', { id: launchRes.data.id }, { reload: true }); + } + }); + } + }); + } else if (vm.job.type === 'job') { + checkRelaunchPlaybook(); } - - RelaunchJob({ scope, id: typeId, type: job.type, name: job.name }); }; vm.relaunchOn = (option) => { - InitiatePlaybookRun({ - scope, - id: job.id, - relaunch: true, - job_type: job.type, - host_type: (option.name).toLowerCase() + checkRelaunchPlaybook(option); + }; + + vm.relaunchJobWithPassword = () => { + jobObj.postRelaunch({ + id: vm.promptData.job, + relaunchData: PromptService.bundlePromptDataForRelaunch(vm.promptData) + }).then((launchRes) => { + if (!$state.includes('jobs')) { + $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); + } + }).catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: `Error relaunching job. POST returned status: ${status}` + }); }); }; } diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html b/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html index 5ce4c45c17..69b3eea711 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html @@ -30,4 +30,5 @@ ng-if="!vm.showDropdown"> -
\ No newline at end of file + + diff --git a/awx/ui/client/lib/models/AdHocCommand.js b/awx/ui/client/lib/models/AdHocCommand.js new file mode 100644 index 0000000000..c398219531 --- /dev/null +++ b/awx/ui/client/lib/models/AdHocCommand.js @@ -0,0 +1,44 @@ +let Base; +let $http; + +function getRelaunch (params) { + const req = { + method: 'GET', + url: `${this.path}${params.id}/relaunch/` + }; + + return $http(req); +} + +function postRelaunch (params) { + const req = { + method: 'POST', + url: `${this.path}${params.id}/relaunch/` + }; + + return $http(req); +} + +function AdHocCommandModel (method, resource, config) { + Base.call(this, 'ad_hoc_commands'); + + this.Constructor = AdHocCommandModel; + this.postRelaunch = postRelaunch.bind(this); + this.getRelaunch = getRelaunch.bind(this); + + return this.create(method, resource, config); +} + +function AdHocCommandModelLoader (BaseModel, _$http_) { + Base = BaseModel; + $http = _$http_; + + return AdHocCommandModel; +} + +AdHocCommandModelLoader.$inject = [ + 'BaseModel', + '$http' +]; + +export default AdHocCommandModelLoader; diff --git a/awx/ui/client/lib/models/InventorySource.js b/awx/ui/client/lib/models/InventorySource.js index 27ec05b6e9..0d68a11780 100644 --- a/awx/ui/client/lib/models/InventorySource.js +++ b/awx/ui/client/lib/models/InventorySource.js @@ -1,5 +1,6 @@ let Base; let WorkflowJobTemplateNode; +let $http; function setDependentResources (id) { this.dependentResources = [ @@ -12,28 +13,51 @@ function setDependentResources (id) { ]; } +function getUpdate (id) { + const req = { + method: 'GET', + url: `${this.path}${id}/update/` + }; + + return $http(req); +} + +function postUpdate (id) { + const req = { + method: 'POST', + url: `${this.path}${id}/update/` + }; + + return $http(req); +} + function InventorySourceModel (method, resource, config) { Base.call(this, 'inventory_sources'); this.Constructor = InventorySourceModel; this.setDependentResources = setDependentResources.bind(this); + this.getUpdate = getUpdate.bind(this); + this.postUpdate = postUpdate.bind(this); return this.create(method, resource, config); } function InventorySourceModelLoader ( BaseModel, - WorkflowJobTemplateNodeModel + WorkflowJobTemplateNodeModel, + _$http_ ) { Base = BaseModel; WorkflowJobTemplateNode = WorkflowJobTemplateNodeModel; + $http = _$http_; return InventorySourceModel; } InventorySourceModelLoader.$inject = [ 'BaseModel', - 'WorkflowJobTemplateNodeModel' + 'WorkflowJobTemplateNodeModel', + '$http' ]; export default InventorySourceModelLoader; diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js index 9be420b2f9..7d87f82330 100644 --- a/awx/ui/client/lib/models/Job.js +++ b/awx/ui/client/lib/models/Job.js @@ -1,21 +1,48 @@ let Base; +let $http; + +function getRelaunch (params) { + const req = { + method: 'GET', + url: `${this.path}${params.id}/relaunch/` + }; + + return $http(req); +} + +function postRelaunch (params) { + const req = { + method: 'POST', + url: `${this.path}${params.id}/relaunch/` + }; + + if (params.relaunchData) { + req.data = params.relaunchData; + } + + return $http(req); +} function JobModel (method, resource, config) { Base.call(this, 'jobs'); this.Constructor = JobModel; + this.postRelaunch = postRelaunch.bind(this); + this.getRelaunch = getRelaunch.bind(this); return this.create(method, resource, config); } -function JobModelLoader (BaseModel) { +function JobModelLoader (BaseModel, _$http_) { Base = BaseModel; + $http = _$http_; return JobModel; } JobModelLoader.$inject = [ - 'BaseModel' + 'BaseModel', + '$http' ]; export default JobModelLoader; diff --git a/awx/ui/client/lib/models/Project.js b/awx/ui/client/lib/models/Project.js index bb08c09179..02606c3dcd 100644 --- a/awx/ui/client/lib/models/Project.js +++ b/awx/ui/client/lib/models/Project.js @@ -2,6 +2,7 @@ let Base; let JobTemplate; let WorkflowJobTemplateNode; let InventorySource; +let $http; function setDependentResources (id) { this.dependentResources = [ @@ -26,11 +27,31 @@ function setDependentResources (id) { ]; } +function getUpdate (id) { + const req = { + method: 'GET', + url: `${this.path}${id}/update/` + }; + + return $http(req); +} + +function postUpdate (id) { + const req = { + method: 'POST', + url: `${this.path}${id}/update/` + }; + + return $http(req); +} + function ProjectModel (method, resource, config) { Base.call(this, 'projects'); this.Constructor = ProjectModel; this.setDependentResources = setDependentResources.bind(this); + this.getUpdate = getUpdate.bind(this); + this.postUpdate = postUpdate.bind(this); return this.create(method, resource, config); } @@ -40,11 +61,13 @@ function ProjectModelLoader ( JobTemplateModel, WorkflowJobTemplateNodeModel, InventorySourceModel, + _$http_ ) { Base = BaseModel; JobTemplate = JobTemplateModel; WorkflowJobTemplateNode = WorkflowJobTemplateNodeModel; InventorySource = InventorySourceModel; + $http = _$http_; return ProjectModel; } @@ -53,7 +76,8 @@ ProjectModelLoader.$inject = [ 'BaseModel', 'JobTemplateModel', 'WorkflowJobTemplateNodeModel', - 'InventorySourceModel' + 'InventorySourceModel', + '$http' ]; export default ProjectModelLoader; diff --git a/awx/ui/client/lib/models/WorkflowJob.js b/awx/ui/client/lib/models/WorkflowJob.js index 06ed451883..02ea3582cc 100644 --- a/awx/ui/client/lib/models/WorkflowJob.js +++ b/awx/ui/client/lib/models/WorkflowJob.js @@ -1,21 +1,34 @@ let Base; +let $http; + +function postRelaunch (params) { + const req = { + method: 'POST', + url: `${this.path}${params.id}/relaunch/` + }; + + return $http(req); +} function WorkflowJobModel (method, resource, config) { Base.call(this, 'workflow_jobs'); this.Constructor = WorkflowJobModel; + this.postRelaunch = postRelaunch.bind(this); return this.create(method, resource, config); } -function WorkflowJobModelLoader (BaseModel) { +function WorkflowJobModelLoader (BaseModel, _$http_) { Base = BaseModel; + $http = _$http_; return WorkflowJobModel; } WorkflowJobModelLoader.$inject = [ - 'BaseModel' + 'BaseModel', + '$http' ]; export default WorkflowJobModelLoader; diff --git a/awx/ui/client/lib/models/WorkflowJobTemplate.js b/awx/ui/client/lib/models/WorkflowJobTemplate.js index d0ff06fc78..79198bbe82 100644 --- a/awx/ui/client/lib/models/WorkflowJobTemplate.js +++ b/awx/ui/client/lib/models/WorkflowJobTemplate.js @@ -1,21 +1,85 @@ let Base; +let $http; + +function optionsLaunch (id) { + const req = { + method: 'OPTIONS', + url: `${this.path}${id}/launch/` + }; + + return $http(req); +} + +function getLaunch (id) { + const req = { + method: 'GET', + url: `${this.path}${id}/launch/` + }; + + return $http(req) + .then(res => { + this.model.launch.GET = res.data; + + return res; + }); +} + +function postLaunch (params) { + const req = { + method: 'POST', + url: `${this.path}${params.id}/launch/` + }; + + if (params.launchData) { + req.data = params.launchData; + } + + return $http(req); +} + +function getSurveyQuestions (id) { + const req = { + method: 'GET', + url: `${this.path}${id}/survey_spec/` + }; + + return $http(req); +} + +function canLaunchWithoutPrompt () { + const launchData = this.model.launch.GET; + + return ( + launchData.can_start_without_user_input && + !launchData.survey_enabled + ); +} function WorkflowJobTemplateModel (method, resource, config) { Base.call(this, 'workflow_job_templates'); this.Constructor = WorkflowJobTemplateModel; + this.optionsLaunch = optionsLaunch.bind(this); + this.getLaunch = getLaunch.bind(this); + this.postLaunch = postLaunch.bind(this); + this.getSurveyQuestions = getSurveyQuestions.bind(this); + this.canLaunchWithoutPrompt = canLaunchWithoutPrompt.bind(this); + + this.model.launch = {}; return this.create(method, resource, config); } -function WorkflowJobTemplateModelLoader (BaseModel) { +function WorkflowJobTemplateModelLoader (BaseModel, _$http_) { Base = BaseModel; + $http = _$http_; return WorkflowJobTemplateModel; } WorkflowJobTemplateModelLoader.$inject = [ - 'BaseModel' + 'BaseModel', + '$http' ]; export default WorkflowJobTemplateModelLoader; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 9f3e57b3cd..3efc8d5299 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -1,6 +1,7 @@ import atLibServices from '~services'; import Application from '~models/Application'; +import AdHocCommand from '~models/AdHocCommand'; import Base from '~models/Base'; import Config from '~models/Config'; import Credential from '~models/Credential'; @@ -30,6 +31,7 @@ angular atLibServices ]) .service('ApplicationModel', Application) + .service('AdHocCommandModel', AdHocCommand) .service('BaseModel', Base) .service('ConfigModel', Config) .service('CredentialModel', Credential) diff --git a/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js index 1a34984a79..7bb40ab40b 100644 --- a/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js +++ b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js @@ -1,10 +1,8 @@ /* jshint unused: vars */ export default - [ 'InitiatePlaybookRun', - 'templateUrl', + [ 'templateUrl', '$state', - 'Alert', - function JobTemplatesList(InitiatePlaybookRun, templateUrl, $state, Alert) { + function JobTemplatesList(templateUrl, $state) { return { restrict: 'E', link: link, @@ -15,6 +13,7 @@ export default }; function link(scope, element, attr) { + scope.$watch("data", function(data) { if (data) { if (data.length > 0) { @@ -30,6 +29,7 @@ export default // smartStatus?, launchUrl, editUrl, name scope.templates = _.map(list, function(template){ return { recent_jobs: template.summary_fields.recent_jobs, + can_start: template.summary_fields.user_capabilities.start, name: template.name, id: template.id, type: template.type @@ -40,25 +40,6 @@ export default return (status === "successful"); }; - scope.launchTemplate = function(template){ - if(template) { - if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) { - InitiatePlaybookRun({ scope: scope, id: template.id, job_type: 'job_template' }); - } - else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) { - InitiatePlaybookRun({ scope: scope, id: template.id, job_type: 'workflow_job_template' }); - } - else { - // Something went wrong - Let the user know that we're unable to launch because we don't know - // what type of job template this is - Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.'); - } - } - else { - Alert('Error: Unable to launch template', 'Template parameter is missing'); - } - }; - scope.editTemplate = function (template) { if(template) { if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) { diff --git a/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.partial.html b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.partial.html index 760f01a08d..c80c4f955e 100644 --- a/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.partial.html +++ b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.partial.html @@ -32,9 +32,9 @@
- + + @@ -56,3 +56,4 @@ You can create a job template here.

+ diff --git a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js index 0d3d79df37..5aa5938714 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js @@ -230,25 +230,25 @@ function adhocController($q, $scope, $stateParams, $scope.removeStartAdhocRun(); } $scope.removeStartAdhocRun = $scope.$on('StartAdhocRun', function() { - var password; - for (password in $scope.passwords) { - data[$scope.passwords[password]] = $scope[ - $scope.passwords[password] - ]; - } - // Launch the adhoc job - Rest.setUrl(GetBasePath('inventory') + id + '/ad_hoc_commands/'); - Rest.post(data) - .then(({data}) => { - Wait('stop'); - $state.go('adHocJobStdout', {id: data.id}); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, adhocForm, { - hdr: 'Error!', - msg: 'Failed to launch adhoc command. POST ' + - 'returned status: ' + status }); - }); + var password; + for (password in $scope.passwords) { + data[$scope.passwords[password]] = $scope[ + $scope.passwords[password] + ]; + } + // Launch the adhoc job + Rest.setUrl(GetBasePath('inventory') + id + '/ad_hoc_commands/'); + Rest.post(data) + .then(({data}) => { + Wait('stop'); + $state.go('adHocJobStdout', {id: data.id}); + }) + .catch(({data, status}) => { + ProcessErrors($scope, data, status, adhocForm, { + hdr: 'Error!', + msg: 'Failed to launch adhoc command. POST ' + + 'returned status: ' + status }); + }); }); if ($scope.removeCreateLaunchDialog) { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js b/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js index 8580524392..5ab048c55e 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js @@ -71,12 +71,8 @@ export default ['i18n', function(i18n) { columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4', submit: { - icon: 'icon-rocket', - mode: 'all', - ngClick: 'relaunchJob($event, completed_job.id)', - awToolTip: i18n._('Relaunch using the same parameters'), - dataPlacement: 'top', ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start", + // uses the at-relaunch directive relaunch: true }, "delete": { diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index b85bdbc8f9..e7e8f2c716 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -237,10 +237,6 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy jobResultsService.cancelJob($scope.job); }; - $scope.relaunchJob = function() { - jobResultsService.relaunchJob($scope); - }; - $scope.lessLabels = false; $scope.toggleLessLabels = function() { if (!$scope.lessLabels) { diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 943730c789..9d8e1a119b 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -22,7 +22,7 @@
- + +
+ +
diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js index e294116d78..899a98e9f7 100644 --- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js +++ b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js @@ -23,5 +23,14 @@ export default { "ad_hoc_command_events": [] } } + }, + resolve: { + jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { + Rest.setUrl(GetBasePath('base') + 'ad_hoc_commands/' + $stateParams.id + '/'); + return Rest.get() + .then(({data}) => { + return data; + }); + }] } }; diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html index 4cd0ec3d82..48f2d65b7e 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -8,7 +8,7 @@ RESULTS
- +
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js index b3931d345c..bdd1a9a2b1 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js @@ -25,5 +25,14 @@ export default { } }, jobType: 'inventory_updates' + }, + resolve: { + jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { + Rest.setUrl(GetBasePath('base') + 'inventory_updates/' + $stateParams.id + '/'); + return Rest.get() + .then(({data}) => { + return data; + }); + }] } }; diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js index 950801990c..e3a59e884d 100644 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js @@ -23,5 +23,14 @@ export default { "system_job_events": [], } } + }, + resolve: { + jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { + Rest.setUrl(GetBasePath('base') + 'system_jobs/' + $stateParams.id + '/'); + return Rest.get() + .then(({data}) => { + return data; + }); + }] } }; diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html index 008516a2ad..f9828d4c02 100644 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html @@ -8,7 +8,7 @@ RESULTS
- +
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js index c2e43be9ca..818509ccc7 100644 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js @@ -25,5 +25,14 @@ export default { "project_update_events": [], } }, + }, + resolve: { + jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { + Rest.setUrl(GetBasePath('base') + 'project_updates/' + $stateParams.id + '/'); + return Rest.get() + .then(({data}) => { + return data; + }); + }] } }; diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/standard-out/standard-out.block.less index bf0ca8727b..fe7e0d2757 100644 --- a/awx/ui/client/src/standard-out/standard-out.block.less +++ b/awx/ui/client/src/standard-out/standard-out.block.less @@ -131,6 +131,10 @@ standard-out-log { font-size: 20px; } +.StandardOut-actions { + display: flex; +} + .StandardOut-actionButton { font-size: 16px; height: 30px; diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index 220eead31c..1a707aa706 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -12,8 +12,9 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName, - ParseTypeChange, ParseVariableString, RelaunchJob, DeleteJob, Wait, i18n, - fieldChoices, fieldLabels) { + ParseTypeChange, ParseVariableString, DeleteJob, Wait, i18n, + fieldChoices, fieldLabels, Project, Alert, InventorySource, + jobData) { var job_id = $stateParams.id, jobType = $state.current.data.jobType; @@ -34,7 +35,12 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') { // Go out and refresh the job details - getjobResults(); + + Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/'); + Rest.get() + .then(({data}) => { + updateJobObj(data); + }); } }); @@ -72,149 +78,142 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, // Set the parse type so that CodeMirror knows how to display extra params YAML/JSON $scope.parseType = 'yaml'; - function getjobResults() { + function updateJobObj(updatedJobData) { // Go out and get the job details based on the job type. jobType gets defined // in the data block of the route declaration for each of the different types // of stdout jobs. - Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/'); - Rest.get() - .then(({data}) => { - $scope.job = data; - $scope.job_template_name = data.name; - $scope.created_by = data.summary_fields.created_by; - $scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : ''; - $scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : ''; - $scope.job_template_url = '/#/templates/' + data.unified_job_template; - if($scope.inventory_name && data.inventory && data.summary_fields.inventory && data.summary_fields.inventory.kind) { - if(data.summary_fields.inventory.kind === '') { - $scope.inventory_url = '/#/inventories/inventory' + data.inventory; - } - else if(data.summary_fields.inventory.kind === 'smart') { - $scope.inventory_url = '/#/inventories/smart_inventory' + data.inventory; - } - } - else { - $scope.inventory_url = ''; - } - $scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : ''; - $scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : ''; - $scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; - $scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; - if(data.summary_fields && data.summary_fields.source_workflow_job && - data.summary_fields.source_workflow_job.id){ - $scope.workflow_result_link = `/#/workflows/${data.summary_fields.source_workflow_job.id}`; - } - $scope.playbook = data.playbook; - $scope.credential = data.credential; - $scope.cloud_credential = data.cloud_credential; - $scope.forks = data.forks; - $scope.limit = data.limit; - $scope.verbosity = data.verbosity; - $scope.job_tags = data.job_tags; - $scope.job.module_name = data.module_name; - if (data.extra_vars) { - $scope.variables = ParseVariableString(data.extra_vars); - } - $scope.$on('getInventorySource', function(e, d) { - $scope.inv_manage_group_link = '/#/inventories/inventory/' + d.inventory + '/inventory_sources/edit/' + d.id; + $scope.job = updatedJobData; + $scope.job_template_name = updatedJobData.name; + $scope.created_by = updatedJobData.summary_fields.created_by; + $scope.project_name = (updatedJobData.summary_fields.project) ? updatedJobData.summary_fields.project.name : ''; + $scope.inventory_name = (updatedJobData.summary_fields.inventory) ? updatedJobData.summary_fields.inventory.name : ''; + $scope.job_template_url = '/#/templates/' + updatedJobData.unified_job_template; + if($scope.inventory_name && updatedJobData.inventory && updatedJobData.summary_fields.inventory && updatedJobData.summary_fields.inventory.kind) { + if(updatedJobData.summary_fields.inventory.kind === '') { + $scope.inventory_url = '/#/inventories/inventory' + updatedJobData.inventory; + } + else if(updatedJobData.summary_fields.inventory.kind === 'smart') { + $scope.inventory_url = '/#/inventories/smart_inventory' + updatedJobData.inventory; + } + } + else { + $scope.inventory_url = ''; + } + $scope.project_url = ($scope.project_name && updatedJobData.project) ? '/#/projects/' + updatedJobData.project : ''; + $scope.credential_name = (updatedJobData.summary_fields.credential) ? updatedJobData.summary_fields.credential.name : ''; + $scope.credential_url = (updatedJobData.credential) ? '/#/credentials/' + updatedJobData.credential : ''; + $scope.cloud_credential_url = (updatedJobData.cloud_credential) ? '/#/credentials/' + updatedJobData.cloud_credential : ''; + if(updatedJobData.summary_fields && updatedJobData.summary_fields.source_workflow_job && + updatedJobData.summary_fields.source_workflow_job.id){ + $scope.workflow_result_link = `/#/workflows/${updatedJobData.summary_fields.source_workflow_job.id}`; + } + $scope.playbook = updatedJobData.playbook; + $scope.credential = updatedJobData.credential; + $scope.cloud_credential = updatedJobData.cloud_credential; + $scope.forks = updatedJobData.forks; + $scope.limit = updatedJobData.limit; + $scope.verbosity = updatedJobData.verbosity; + $scope.job_tags = updatedJobData.job_tags; + $scope.job.module_name = updatedJobData.module_name; + if (updatedJobData.extra_vars) { + $scope.variables = ParseVariableString(updatedJobData.extra_vars); + } + + $scope.$on('getInventorySource', function(e, d) { + $scope.inv_manage_group_link = '/#/inventories/inventory/' + d.inventory + '/inventory_sources/edit/' + d.id; + }); + + // If we have a source then we have to go get the source choices from the server + if (!Empty(updatedJobData.source)) { + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('ChoicesReady', function() { + $scope.source_choices.every(function(e) { + if (e.value === updatedJobData.source) { + $scope.source = e.label; + return false; + } + return true; }); - - // If we have a source then we have to go get the source choices from the server - if (!Empty(data.source)) { - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('ChoicesReady', function() { - $scope.source_choices.every(function(e) { - if (e.value === data.source) { - $scope.source = e.label; - return false; - } - return true; - }); - }); - // GetChoices can be found in the helper: Utilities.js - // It attaches the source choices to $scope.source_choices. - // Then, when the callback is fired, $scope.source is bound - // to the corresponding label. - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source', - variable: 'source_choices', - choice_name: 'choices', - callback: 'ChoicesReady' - }); - } - - // LookUpName can be found in the lookup-name.factory - // It attaches the name that it gets (based on the url) - // to the $scope variable defined by the attribute scope_var. - if (!Empty(data.credential)) { - LookUpName({ - scope: $scope, - scope_var: 'credential', - url: GetBasePath('credentials') + data.credential + '/', - ignore_403: true - }); - } - - if (!Empty(data.inventory)) { - LookUpName({ - scope: $scope, - scope_var: 'inventory', - url: GetBasePath('inventory') + data.inventory + '/' - }); - } - - if (!Empty(data.project)) { - LookUpName({ - scope: $scope, - scope_var: 'project', - url: GetBasePath('projects') + data.project + '/' - }); - } - - if (!Empty(data.cloud_credential)) { - LookUpName({ - scope: $scope, - scope_var: 'cloud_credential', - url: GetBasePath('credentials') + data.cloud_credential + '/', - ignore_403: true - }); - } - - if (!Empty(data.inventory_source)) { - LookUpName({ - scope: $scope, - scope_var: 'inventory_source', - url: GetBasePath('inventory_sources') + data.inventory_source + '/', - callback: 'getInventorySource' - }); - } - - if (data.extra_vars) { - ParseTypeChange({ - scope: $scope, - field_id: 'pre-formatted-variables', - readOnly: true - }); - } - - // If the job isn't running we want to clear out the interval that goes out and checks for stdout updates. - // This interval is defined in the standard out log directive controller. - if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { - if ($rootScope.jobStdOutInterval) { - window.clearInterval($rootScope.jobStdOutInterval); - } - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status }); }); + // GetChoices can be found in the helper: Utilities.js + // It attaches the source choices to $scope.source_choices. + // Then, when the callback is fired, $scope.source is bound + // to the corresponding label. + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source', + variable: 'source_choices', + choice_name: 'choices', + callback: 'ChoicesReady' + }); + } + + // LookUpName can be found in the lookup-name.factory + // It attaches the name that it gets (based on the url) + // to the $scope variable defined by the attribute scope_var. + if (!Empty(updatedJobData.credential)) { + LookUpName({ + scope: $scope, + scope_var: 'credential', + url: GetBasePath('credentials') + updatedJobData.credential + '/', + ignore_403: true + }); + } + + if (!Empty(updatedJobData.inventory)) { + LookUpName({ + scope: $scope, + scope_var: 'inventory', + url: GetBasePath('inventory') + updatedJobData.inventory + '/' + }); + } + + if (!Empty(updatedJobData.project)) { + LookUpName({ + scope: $scope, + scope_var: 'project', + url: GetBasePath('projects') + updatedJobData.project + '/' + }); + } + + if (!Empty(updatedJobData.cloud_credential)) { + LookUpName({ + scope: $scope, + scope_var: 'cloud_credential', + url: GetBasePath('credentials') + updatedJobData.cloud_credential + '/', + ignore_403: true + }); + } + + if (!Empty(updatedJobData.inventory_source)) { + LookUpName({ + scope: $scope, + scope_var: 'inventory_source', + url: GetBasePath('inventory_sources') + updatedJobData.inventory_source + '/', + callback: 'getInventorySource' + }); + } + + if (updatedJobData.extra_vars) { + ParseTypeChange({ + scope: $scope, + field_id: 'pre-formatted-variables', + readOnly: true + }); + } + + // If the job isn't running we want to clear out the interval that goes out and checks for stdout updates. + // This interval is defined in the standard out log directive controller. + if (updatedJobData.status === 'successful' || updatedJobData.status === 'failed' || updatedJobData.status === 'error' || updatedJobData.status === 'canceled') { + if ($rootScope.jobStdOutInterval) { + window.clearInterval($rootScope.jobStdOutInterval); + } + } } @@ -255,26 +254,13 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, }); }; - $scope.relaunchJob = function() { - var typeId, job = $scope.job; - if (job.type === 'inventory_update') { - typeId = job.inventory_source; - } - else if (job.type === 'project_update') { - typeId = job.project; - } - else if (job.type === 'job' || job.type === "system_job" || job.type === 'ad_hoc_command') { - typeId = job.id; - } - RelaunchJob({ scope: $scope, id: typeId, type: job.type, name: job.name }); - }; - - getjobResults(); + updateJobObj(jobData); } JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange', - 'ParseVariableString', 'RelaunchJob', 'DeleteJob', 'Wait', 'i18n', - 'fieldChoices', 'fieldLabels']; + 'ParseVariableString', 'DeleteJob', 'Wait', 'i18n', + 'fieldChoices', 'fieldLabels', 'ProjectModel', 'Alert', 'InventorySourceModel', + 'jobData']; diff --git a/awx/ui/client/src/templates/completed-jobs.list.js b/awx/ui/client/src/templates/completed-jobs.list.js index 8be715f2dd..54ba53a48e 100644 --- a/awx/ui/client/src/templates/completed-jobs.list.js +++ b/awx/ui/client/src/templates/completed-jobs.list.js @@ -72,12 +72,9 @@ export default ['i18n', function(i18n) { columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4', submit: { - icon: 'icon-rocket', - mode: 'all', - ngClick: 'relaunchJob($event, completed_job.id)', - awToolTip: i18n._('Relaunch using the same parameters'), - dataPlacement: 'top', - ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start" + ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start", + // uses the at-relaunch directive + relaunch: true }, "delete": { mode: 'all', diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index 38b32b2942..3e12551fe3 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -16,7 +16,7 @@ export default 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'md5Setup', 'ParseTypeChange', 'Wait', 'selectedLabels', 'i18n', 'Empty', 'Prompt', 'ToJSON', 'GetChoices', 'CallbackHelpInit', - 'InitiatePlaybookRun' , 'initSurvey', '$state', 'CreateSelect2', + 'initSurvey', '$state', 'CreateSelect2', 'ToggleNotification','$q', 'InstanceGroupsService', 'InstanceGroupsData', 'MultiCredentialService', 'availableLabels', 'projectGetPermissionDenied', 'inventoryGetPermissionDenied', 'jobTemplateData', 'ParseVariableString', 'ConfigData', @@ -26,7 +26,7 @@ export default ProcessErrors, GetBasePath, md5Setup, ParseTypeChange, Wait, selectedLabels, i18n, Empty, Prompt, ToJSON, GetChoices, CallbackHelpInit, - InitiatePlaybookRun, SurveyControllerInit, $state, CreateSelect2, + SurveyControllerInit, $state, CreateSelect2, ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData, MultiCredentialService, availableLabels, projectGetPermissionDenied, inventoryGetPermissionDenied, jobTemplateData, ParseVariableString, ConfigData @@ -41,7 +41,6 @@ export default let defaultUrl = GetBasePath('job_templates'), generator = GenerateForm, form = JobTemplateForm(), - base = $location.path().replace(/^\//, '').split('/')[0], master = {}, id = $stateParams.job_template_id, callback, @@ -718,50 +717,5 @@ export default $scope.formCancel = function () { $state.go('templates'); }; - - // Related set: Add button - $scope.add = function (set) { - $rootScope.flashMessage = null; - $location.path('/' + base + '/' + $stateParams.job_template_id + '/' + set); - }; - - // Related set: Edit button - $scope.edit = function (set, id) { - $rootScope.flashMessage = null; - $location.path('/' + set + '/' + id); - }; - - // Launch a job using the selected template - $scope.launch = function() { - - if ($scope.removePromptForSurvey) { - $scope.removePromptForSurvey(); - } - $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { - var action = function () { - // $scope.$emit("GatherFormFields"); - Wait('start'); - $('#prompt-modal').modal('hide'); - $scope.addSurvey(); - - }; - Prompt({ - hdr: 'Incomplete Survey', - body: '
Do you want to create a survey before proceeding?
', - action: action - }); - }); - if($scope.survey_enabled === true && $scope.survey_exists!==true){ - $scope.$emit("PromptForSurvey"); - } - else { - - InitiatePlaybookRun({ - scope: $scope, - id: id, - job_type: 'job_template' - }); - } - }; } ]; diff --git a/awx/ui/client/src/templates/prompt/prompt.controller.js b/awx/ui/client/src/templates/prompt/prompt.controller.js index f2f08f9207..79df58c51c 100644 --- a/awx/ui/client/src/templates/prompt/prompt.controller.js +++ b/awx/ui/client/src/templates/prompt/prompt.controller.js @@ -65,37 +65,30 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', } })); - vm.promptData.prompts.credentials.passwordsNeededToStart = vm.promptData.launchConf.passwords_needed_to_start; vm.promptData.prompts.credentials.passwords = {}; - vm.promptData.prompts.credentials.value.forEach((credential) => { - if (credential.passwords_needed && credential.passwords_needed.length > 0) { - credential.passwords_needed.forEach(passwordNeeded => { - let credPassObj = { - id: credential.id, - name: credential.name - }; + vm.promptData.launchConf.passwords_needed_to_start.forEach((passwordNeeded) => { + if(passwordNeeded === "ssh_password") { + vm.promptData.prompts.credentials.passwords.ssh = {}; + } + if(passwordNeeded === "become_password") { + vm.promptData.prompts.credentials.passwords.become = {}; + } + if(passwordNeeded === "ssh_key_unlock") { + vm.promptData.prompts.credentials.passwords.ssh_key_unlock = {}; + } + if(passwordNeeded.startsWith("vault_password")) { + let vault_id; + if(passwordNeeded.includes('.')) { + vault_id = passwordNeeded.split(/\.(.+)/)[1]; + } - if(passwordNeeded === "ssh_password") { - vm.promptData.prompts.credentials.passwords.ssh = credPassObj; - } - if(passwordNeeded === "become_password") { - vm.promptData.prompts.credentials.passwords.become = credPassObj; - } - if(passwordNeeded === "ssh_key_unlock") { - vm.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj; - } - if(passwordNeeded.startsWith("vault_password")) { - if(passwordNeeded.includes('.')) { - credPassObj.vault_id = passwordNeeded.split(/\.(.+)/)[1]; - } + if(!vm.promptData.prompts.credentials.passwords.vault) { + vm.promptData.prompts.credentials.passwords.vault = []; + } - if(!vm.promptData.prompts.credentials.passwords.vault) { - vm.promptData.prompts.credentials.passwords.vault = []; - } - - vm.promptData.prompts.credentials.passwords.vault.push(credPassObj); - } + vm.promptData.prompts.credentials.passwords.vault.push({ + vault_id: vault_id }); } }); diff --git a/awx/ui/client/src/templates/prompt/prompt.service.js b/awx/ui/client/src/templates/prompt/prompt.service.js index 5ab5002c27..2044b7f440 100644 --- a/awx/ui/client/src/templates/prompt/prompt.service.js +++ b/awx/ui/client/src/templates/prompt/prompt.service.js @@ -1,7 +1,7 @@ function PromptService (Empty, $filter) { this.processPromptValues = (params) => { - let prompts = { + const prompts = { credentials: {}, inventory: {}, variables: {}, @@ -16,10 +16,23 @@ function PromptService (Empty, $filter) { prompts.credentials.value = _.has(params, 'launchConf.defaults.credentials') ? _.cloneDeep(params.launchConf.defaults.credentials) : []; prompts.inventory.value = _.has(params, 'currentValues.summary_fields.inventory') ? params.currentValues.summary_fields.inventory : (_.has(params, 'launchConf.defaults.inventory') ? params.launchConf.defaults.inventory : null); - let skipTags = _.has(params, 'currentValues.skip_tags') && params.currentValues.skip_tags ? params.currentValues.skip_tags : (_.has(params, 'launchConf.defaults.skip_tags') ? params.launchConf.defaults.skip_tags : ""); - let jobTags = _.has(params, 'currentValues.job_tags') && params.currentValues.job_tags ? params.currentValues.job_tags : (_.has(params, 'launchConf.defaults.job_tags') ? params.launchConf.defaults.job_tags : ""); + const skipTags = _.has(params, 'currentValues.skip_tags') && params.currentValues.skip_tags ? params.currentValues.skip_tags : (_.has(params, 'launchConf.defaults.skip_tags') ? params.launchConf.defaults.skip_tags : ""); + const jobTags = _.has(params, 'currentValues.job_tags') && params.currentValues.job_tags ? params.currentValues.job_tags : (_.has(params, 'launchConf.defaults.job_tags') ? params.launchConf.defaults.job_tags : ""); - prompts.variables.value = _.has(params, 'launchConf.defaults.extra_vars') && params.launchConf.defaults.extra_vars !== "" ? params.launchConf.defaults.extra_vars : "---"; + let extraVars = ''; + + const hasCurrentExtraVars = _.get(params, 'currentValues.extra_data'), + hasDefaultExtraVars = _.get(params, 'launchConf.defaults.extra_vars'); + + if(hasCurrentExtraVars && hasDefaultExtraVars) { + extraVars = _.merge(jsyaml.safeLoad(params.launchConf.defaults.extra_vars), params.currentValues.extra_data); + } else if(hasCurrentExtraVars) { + extraVars = params.currentValues.extra_data; + } else if(hasDefaultExtraVars) { + extraVars = jsyaml.safeLoad(params.launchConf.defaults.extra_vars); + } + + prompts.variables.value = extraVars && extraVars !== '' ? '---\n' + jsyaml.safeDump(extraVars) : '---\n'; prompts.verbosity.choices = _.get(params, 'launchOptions.actions.POST.verbosity.choices', []).map(c => ({label: c[1], value: c[0]})); prompts.verbosity.value = _.has(params, 'currentValues.verbosity') && params.currentValues.verbosity ? _.find(prompts.verbosity.choices, item => item.value === params.currentValues.verbosity) : _.find(prompts.verbosity.choices, item => item.value === params.launchConf.defaults.verbosity); prompts.jobType.choices = _.get(params, 'launchOptions.actions.POST.job_type.choices', []).map(c => ({label: c[1], value: c[0]})); @@ -27,7 +40,7 @@ function PromptService (Empty, $filter) { prompts.limit.value = _.has(params, 'currentValues.limit') && params.currentValues.limit ? params.currentValues.limit : (_.has(params, 'launchConf.defaults.limit') ? params.launchConf.defaults.limit : ""); prompts.tags.options = prompts.tags.value = (jobTags && jobTags !== "") ? jobTags.split(',').map((i) => ({name: i, label: i, value: i})) : []; prompts.skipTags.options = prompts.skipTags.value = (skipTags && skipTags !== "") ? skipTags.split(',').map((i) => ({name: i, label: i, value: i})) : []; - prompts.diffMode.value = _.has(params, 'currentValues.diff_mode') && typeof params.currentValues.diff_mode === 'boolean' ? params.currentValues.diff_mode : (_.has(params, 'launchConf.defaults.diff_mode') ? params.launchConf.defaults.diff_mode : false); + prompts.diffMode.value = _.has(params, 'currentValues.diff_mode') && typeof params.currentValues.diff_mode === 'boolean' ? params.currentValues.diff_mode : (_.has(params, 'launchConf.defaults.diff_mode') ? params.launchConf.defaults.diff_mode : null); return prompts; }; @@ -117,6 +130,144 @@ function PromptService (Empty, $filter) { missingSurveyValue: missingSurveyValue }; }; + + this.bundlePromptDataForLaunch = (promptData) => { + const launchData = { + extra_vars: promptData.extraVars + }; + + if (promptData.launchConf.ask_tags_on_launch){ + launchData.job_tags = promptData.prompts.tags.value.map(a => a.value).join(); + } + if (promptData.launchConf.ask_skip_tags_on_launch){ + launchData.skip_tags = promptData.prompts.skipTags.value.map(a => a.value).join(); + } + if (promptData.launchConf.ask_limit_on_launch && _.has(promptData, 'prompts.limit.value')){ + launchData.limit = promptData.prompts.limit.value; + } + if (promptData.launchConf.ask_job_type_on_launch && _.has(promptData, 'prompts.jobType.value.value')) { + launchData.job_type = promptData.prompts.jobType.value.value; + } + if (promptData.launchConf.ask_verbosity_on_launch && _.has(promptData, 'prompts.verbosity.value.value')) { + launchData.verbosity = promptData.prompts.verbosity.value.value; + } + if (promptData.launchConf.ask_inventory_on_launch && !Empty(promptData.prompts.inventory.value.id)){ + launchData.inventory_id = promptData.prompts.inventory.value.id; + } + if (promptData.launchConf.ask_credential_on_launch){ + launchData.credentials = []; + promptData.prompts.credentials.value.forEach((credential) => { + launchData.credentials.push(credential.id); + }); + } + if (promptData.launchConf.ask_diff_mode_on_launch && _.has(promptData, 'prompts.diffMode.value')) { + launchData.diff_mode = promptData.prompts.diffMode.value; + } + if (promptData.prompts.credentials.passwords) { + _.forOwn(promptData.prompts.credentials.passwords, (val, key) => { + if (!launchData.credential_passwords) { + launchData.credential_passwords = {}; + } + if (key === "ssh_key_unlock") { + launchData.credential_passwords.ssh_key_unlock = val.value; + } else if (key !== "vault") { + launchData.credential_passwords[`${key}_password`] = val.value; + } else { + _.each(val, (vaultCred) => { + launchData.credential_passwords[vaultCred.vault_id ? `${key}_password.${vaultCred.vault_id}` : `${key}_password`] = vaultCred.value; + }); + } + }); + } + + return launchData; + }; + + this.bundlePromptDataForRelaunch = (promptData) => { + const launchData = {}; + + if(promptData.relaunchHostType) { + launchData.hosts = promptData.relaunchHostType; + } + + if (promptData.prompts.credentials.passwords) { + _.forOwn(promptData.prompts.credentials.passwords, (val, key) => { + if (!launchData.credential_passwords) { + launchData.credential_passwords = {}; + } + if (key === "ssh_key_unlock") { + launchData.credential_passwords.ssh_key_unlock = val.value; + } else if (key !== "vault") { + launchData.credential_passwords[`${key}_password`] = val.value; + } else { + _.each(val, (vaultCred) => { + launchData.credential_passwords[vaultCred.vault_id ? `${key}_password.${vaultCred.vault_id}` : `${key}_password`] = vaultCred.value; + }); + } + }); + } + + return launchData; + }; + + this.bundlePromptDataForSaving = (params) => { + + const promptDataToSave = params.dataToSave ? params.dataToSave : {}; + + if(params.promptData.launchConf.survey_enabled){ + for (var i=0; i < params.promptData.surveyQuestions.length; i++){ + var fld = params.promptData.surveyQuestions[i].variable; + // grab all survey questions that have answers + if(params.promptData.surveyQuestions[i].required || (params.promptData.surveyQuestions[i].required === false && params.promptData.surveyQuestions[i].model.toString()!=="")) { + if(!promptDataToSave.extra_data) { + promptDataToSave.extra_data = {}; + } + promptDataToSave.extra_data[fld] = params.promptData.surveyQuestions[i].model; + } + + if(params.promptData.surveyQuestions[i].required === false && _.isEmpty(params.promptData.surveyQuestions[i].model)) { + switch (params.promptData.surveyQuestions[i].type) { + // for optional text and text-areas, submit a blank string if min length is 0 + // -- this is confusing, for an explanation see: + // http://docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html#optional-survey-questions + // + case "text": + case "textarea": + if (params.promptData.surveyQuestions[i].min === 0) { + promptDataToSave.extra_data[fld] = ""; + } + break; + } + } + } + } + + if(_.has(params, 'promptData.prompts.jobType.value.value') && _.get(params, 'promptData.launchConf.ask_job_type_on_launch')) { + promptDataToSave.job_type = params.promptData.launchConf.defaults.job_type && params.promptData.launchConf.defaults.job_type === params.promptData.prompts.jobType.value.value ? null : params.promptData.prompts.jobType.value.value; + } + if(_.has(params, 'promptData.prompts.tags.value') && _.get(params, 'promptData.launchConf.ask_tags_on_launch')){ + const templateDefaultJobTags = params.promptData.launchConf.defaults.job_tags.split(','); + promptDataToSave.job_tags = (_.isEqual(templateDefaultJobTags.sort(), params.promptData.prompts.tags.value.map(a => a.value).sort())) ? null : params.promptData.prompts.tags.value.map(a => a.value).join(); + } + if(_.has(params, 'promptData.prompts.skipTags.value') && _.get(params, 'promptData.launchConf.ask_skip_tags_on_launch')){ + const templateDefaultSkipTags = params.promptData.launchConf.defaults.skip_tags.split(','); + promptDataToSave.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), params.promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : params.promptData.prompts.skipTags.value.map(a => a.value).join(); + } + if(_.has(params, 'promptData.prompts.limit.value') && _.get(params, 'promptData.launchConf.ask_limit_on_launch')){ + promptDataToSave.limit = params.promptData.launchConf.defaults.limit && params.promptData.launchConf.defaults.limit === params.promptData.prompts.limit.value ? null : params.promptData.prompts.limit.value; + } + if(_.has(params, 'promptData.prompts.verbosity.value.value') && _.get(params, 'promptData.launchConf.ask_verbosity_on_launch')){ + promptDataToSave.verbosity = params.promptData.launchConf.defaults.verbosity && params.promptData.launchConf.defaults.verbosity === params.promptData.prompts.verbosity.value.value ? null : params.promptData.prompts.verbosity.value.value; + } + if(_.has(params, 'promptData.prompts.inventory.value') && _.get(params, 'promptData.launchConf.ask_inventory_on_launch')){ + promptDataToSave.inventory = params.promptData.launchConf.defaults.inventory && params.promptData.launchConf.defaults.inventory.id === params.promptData.prompts.inventory.value.id ? null : params.promptData.prompts.inventory.value.id; + } + if(_.has(params, 'promptData.prompts.diffMode.value') && _.get(params, 'promptData.launchConf.ask_diff_mode_on_launch')){ + promptDataToSave.diff_mode = params.promptData.launchConf.defaults.diff_mode && params.promptData.launchConf.defaults.diff_mode === params.promptData.prompts.diffMode.value ? null : params.promptData.prompts.diffMode.value; + } + + return promptDataToSave; + }; } PromptService.$inject = ['Empty', '$filter']; diff --git a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.controller.js b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.controller.js index 66e7e00600..eb88095284 100644 --- a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.controller.js +++ b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.controller.js @@ -72,7 +72,7 @@ export default }); } - scope.promptExtraVars = $.isEmptyObject(scope.promptData.extraVars) ? '---' : jsyaml.safeDump(scope.promptData.extraVars); + scope.promptExtraVars = $.isEmptyObject(scope.promptData.extraVars) ? '---' : '---\n' + jsyaml.safeDump(scope.promptData.extraVars); ParseTypeChange({ scope: scope, diff --git a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html index b2a20a541f..452aa6815c 100644 --- a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html @@ -1,5 +1,5 @@
-
+
{{:: vm.strings.get('prompt.JOB_TYPE') }}
{{:: vm.strings.get('prompt.PLAYBOOK_RUN') }} @@ -24,7 +24,7 @@
-
+
{{:: vm.strings.get('prompt.INVENTORY') }}
@@ -32,7 +32,7 @@
{{:: vm.strings.get('prompt.LIMIT') }}
-
+
{{:: vm.strings.get('prompt.VERBOSITY') }}
@@ -68,7 +68,7 @@
-
+
{{:: vm.strings.get('prompt.SHOW_CHANGES') }}
{{:: vm.strings.get('ON') }} 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 85e0336352..8f80fadf84 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 @@ -77,61 +77,36 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', unified_job_template: params.node.unifiedJobTemplate.id }; + if(_.has(params, 'node.promptData.extraVars')) { + if(_.get(params, 'node.promptData.launchConf.defaults.extra_vars')) { + if(!sendableNodeData.extra_data) { + sendableNodeData.extra_data = {}; + } + + const defaultVars = jsyaml.safeLoad(params.node.promptData.launchConf.defaults.extra_vars); + + // Only include extra vars that differ from the template default vars + _.forOwn(params.node.promptData.extraVars, (value, key) => { + if(!defaultVars[key] || defaultVars[key] !== value) { + sendableNodeData.extra_data[key] = value; + } + }); + if(sendableNodeData.extra_data === {}) { + delete sendableNodeData.extra_data; + } + } else { + sendableNodeData.extra_data = params.node.promptData.extraVars; + } + } + // Check to see if the user has provided any prompt values that are different // from the defaults in the job template if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptData) { - if(params.node.promptData.launchConf.survey_enabled){ - for (var i=0; i < params.node.promptData.surveyQuestions.length; i++){ - var fld = params.node.promptData.surveyQuestions[i].variable; - // grab all survey questions that have answers - if(params.node.promptData.surveyQuestions[i].required || (params.node.promptData.surveyQuestions[i].required === false && params.node.promptData.surveyQuestions[i].model.toString()!=="")) { - if(!sendableNodeData.extra_data) { - sendableNodeData.extra_data = {}; - } - sendableNodeData.extra_data[fld] = params.node.promptData.surveyQuestions[i].model; - } - - if(params.node.promptData.surveyQuestions[i].required === false && _.isEmpty(params.node.promptData.surveyQuestions[i].model)) { - switch (params.node.promptData.surveyQuestions[i].type) { - // for optional text and text-areas, submit a blank string if min length is 0 - // -- this is confusing, for an explanation see: - // http://docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html#optional-survey-questions - // - case "text": - case "textarea": - if (params.node.promptData.surveyQuestions[i].min === 0) { - sendableNodeData.extra_data[fld] = ""; - } - break; - } - } - } - } - - if(_.has(params, 'node.promptData.prompts.jobType.value.value') && _.get(params, 'node.promptData.launchConf.ask_job_type_on_launch')) { - sendableNodeData.job_type = params.node.promptData.prompts.jobType.templateDefault === params.node.promptData.prompts.jobType.value.value ? null : params.node.promptData.prompts.jobType.value.value; - } - if(_.has(params, 'node.promptData.prompts.tags.value') && _.get(params, 'node.promptData.launchConf.ask_tags_on_launch')){ - let templateDefaultJobTags = params.node.promptData.prompts.tags.templateDefault.split(','); - sendableNodeData.job_tags = (_.isEqual(templateDefaultJobTags.sort(), params.node.promptData.prompts.tags.value.map(a => a.value).sort())) ? null : params.node.promptData.prompts.tags.value.map(a => a.value).join(); - } - if(_.has(params, 'node.promptData.prompts.skipTags.value') && _.get(params, 'node.promptData.launchConf.ask_skip_tags_on_launch')){ - let templateDefaultSkipTags = params.node.promptData.prompts.skipTags.templateDefault.split(','); - sendableNodeData.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), params.node.promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : params.node.promptData.prompts.skipTags.value.map(a => a.value).join(); - } - if(_.has(params, 'node.promptData.prompts.limit.value') && _.get(params, 'node.promptData.launchConf.ask_limit_on_launch')){ - sendableNodeData.limit = params.node.promptData.prompts.limit.templateDefault === params.node.promptData.prompts.limit.value ? null : params.node.promptData.prompts.limit.value; - } - if(_.has(params, 'node.promptData.prompts.verbosity.value.value') && _.get(params, 'node.promptData.launchConf.ask_verbosity_on_launch')){ - sendableNodeData.verbosity = params.node.promptData.prompts.verbosity.templateDefault === params.node.promptData.prompts.verbosity.value.value ? null : params.node.promptData.prompts.verbosity.value.value; - } - if(_.has(params, 'node.promptData.prompts.inventory.value') && _.get(params, 'node.promptData.launchConf.ask_inventory_on_launch')){ - sendableNodeData.inventory = _.has(params, 'node.promptData.prompts.inventory.templateDefault.id') && params.node.promptData.prompts.inventory.templateDefault.id === params.node.promptData.prompts.inventory.value.id ? null : params.node.promptData.prompts.inventory.value.id; - } - if(_.has(params, 'node.promptData.prompts.diffMode.value') && _.get(params, 'node.promptData.launchConf.ask_diff_mode_on_launch')){ - sendableNodeData.diff_mode = params.node.promptData.prompts.diffMode.templateDefault === params.node.promptData.prompts.diffMode.value ? null : params.node.promptData.prompts.diffMode.value; - } + sendableNodeData = PromptService.bundlePromptDataForSaving({ + promptData: params.node.promptData, + dataToSave: sendableNodeData + }); } return sendableNodeData; @@ -1028,7 +1003,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }), }; - $scope.$watch('promptData.surveyQuestions', () => { + surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => { let missingSurveyValue = false; _.each($scope.promptData.surveyQuestions, (question) => { if(question.required && (Empty(question.model) || question.model === [])) { diff --git a/awx/ui/client/src/workflow-results/workflow-results.service.js b/awx/ui/client/src/workflow-results/workflow-results.service.js index 4db058a163..41bc410647 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.service.js +++ b/awx/ui/client/src/workflow-results/workflow-results.service.js @@ -5,7 +5,7 @@ *************************************************/ -export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'InitiatePlaybookRun', '$interval', 'moment', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, InitiatePlaybookRun, $interval, moment) { +export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'WorkflowJobModel', '$interval', 'moment', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, WorkflowJob, $interval, moment) { var val = { getCounts: function(workflowNodes){ var nodeArr = []; @@ -107,8 +107,13 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr }); }, relaunchJob: function(scope) { - InitiatePlaybookRun({ scope: scope, id: scope.workflow.id, - relaunch: true, job_type: 'workflow_job' }); + const workflowJob = new WorkflowJob(); + + workflowJob.postRelaunch({ + id: scope.workflow.id + }).then((launchRes) => { + $state.go('workflowResults', { id: launchRes.data.id }, { reload: true }); + }); }, createOneSecondTimer: function(startTime, fn) { return $interval(function(){ diff --git a/awx/ui/test/spec/job-results/job-results.controller-test.js b/awx/ui/test/spec/job-results/job-results.controller-test.js index af3aeeae34..c05f2329f7 100644 --- a/awx/ui/test/spec/job-results/job-results.controller-test.js +++ b/awx/ui/test/spec/job-results/job-results.controller-test.js @@ -393,19 +393,6 @@ describe('Controller: jobResultsController', () => { }); }); - describe('$scope.relaunchJob', () => { - beforeEach(() => { - bootstrapTest(); - }); - - it('should relaunch the job', () => { - let scope = $scope; - $scope.relaunchJob(); - expect(jobResultsService.relaunchJob) - .toHaveBeenCalledWith(scope); - }); - }); - describe('count stuff', () => { beforeEach(() => { count = { diff --git a/awx/ui/test/spec/workflow--results/workflow-results.controller-test.js b/awx/ui/test/spec/workflow--results/workflow-results.controller-test.js index 91616b8a03..4976ce1306 100644 --- a/awx/ui/test/spec/workflow--results/workflow-results.controller-test.js +++ b/awx/ui/test/spec/workflow--results/workflow-results.controller-test.js @@ -18,7 +18,7 @@ describe('Controller: workflowResults', () => { beforeEach(angular.mock.module('workflowResults', ($provide) => { ['PromptDialog', 'Prompt', 'Wait', 'Rest', '$state', 'ProcessErrors', - 'InitiatePlaybookRun', 'jobLabels', 'workflowNodes', 'count', + 'jobLabels', 'workflowNodes', 'count', 'WorkflowJobModel', ].forEach((item) => { $provide.value(item, {}); }); diff --git a/awx/ui/test/spec/workflow--results/workflow-results.service-test.js b/awx/ui/test/spec/workflow--results/workflow-results.service-test.js index 8f0e2ad843..c730fc0dcf 100644 --- a/awx/ui/test/spec/workflow--results/workflow-results.service-test.js +++ b/awx/ui/test/spec/workflow--results/workflow-results.service-test.js @@ -6,9 +6,10 @@ describe('workflowResultsService', () => { let $interval; beforeEach(angular.mock.module('workflowResults', ($provide) => { - ['PromptDialog', 'Prompt', 'Wait', 'Rest', 'ProcessErrors', 'InitiatePlaybookRun', '$state'].forEach(function(item) { - $provide.value(item, {}); - }); + ['PromptDialog', 'Prompt', 'Wait', 'Rest', 'ProcessErrors', '$state', 'WorkflowJobModel'] + .forEach(function(item) { + $provide.value(item, {}); + }); $provide.value('$stateExtender', { addState: jasmine.createSpy('addState'), }); $provide.value('moment', moment); }));