From e57d200d6ea621ee9050f6fe840e58fdf06c0ab8 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 31 Jan 2018 15:28:52 -0500 Subject: [PATCH] Implemented generic prompt modal for launching and saving launch configurations. Added UI support for prompting on job template schedules. --- .../add-edit-credentials.view.html | 1 - .../templates/list-templates.controller.js | 159 ++++++- .../client/features/templates/list.view.html | 1 + .../features/templates/templates.strings.js | 41 +- awx/ui/client/legacy/styles/forms.less | 13 + .../lib/components/modal/modal.directive.js | 18 +- .../lib/components/tabs/group.directive.js | 6 + .../lib/components/tabs/tab.directive.js | 7 +- awx/ui/client/lib/models/Job.js | 21 + awx/ui/client/lib/models/JobTemplate.js | 78 +++- awx/ui/client/lib/models/WorkflowJob.js | 21 + awx/ui/client/lib/models/index.js | 7 +- .../lib/services/base-string.service.js | 7 + awx/ui/client/lib/theme/index.less | 1 + .../relatedGroupsLabelsList.partial.html | 6 +- .../job-submission.controller.js | 15 +- .../job-submission.partial.html | 18 +- .../factories/add-schedule.factory.js | 135 ------ .../factories/edit-schedule.factory.js | 154 ------- .../factories/schedule-post.factory.js | 94 ++++- awx/ui/client/src/scheduler/main.js | 12 +- .../src/scheduler/schedulerAdd.controller.js | 350 +++++++++++---- .../src/scheduler/schedulerEdit.controller.js | 398 ++++++++++++++---- .../src/scheduler/schedulerForm.block.less | 12 + .../src/scheduler/schedulerForm.partial.html | 45 +- .../instance-groups.partial.html | 6 +- .../list-generator/list-generator.factory.js | 6 +- .../shared/parse/parse-type-change.factory.js | 2 + .../multi-credential-modal.partial.html | 12 +- .../multi-credential.block.less | 41 +- .../multi-credential.partial.html | 11 +- .../templates/labels/labelsList.block.less | 18 +- .../templates/labels/labelsList.partial.html | 6 +- awx/ui/client/src/templates/main.js | 3 +- awx/ui/client/src/templates/prompt/main.js | 17 + .../src/templates/prompt/prompt.block.less | 166 ++++++++ .../src/templates/prompt/prompt.controller.js | 188 +++++++++ .../src/templates/prompt/prompt.directive.js | 24 ++ .../src/templates/prompt/prompt.partial.html | 36 ++ .../src/templates/prompt/prompt.service.js | 124 ++++++ .../prompt-credential.controller.js | 273 ++++++++++++ .../credential/prompt-credential.directive.js | 56 +++ .../credential/prompt-credential.partial.html | 113 +++++ .../inventory/prompt-inventory.controller.js | 37 ++ .../inventory/prompt-inventory.directive.js | 72 ++++ .../inventory/prompt-inventory.partial.html | 26 ++ .../prompt-other-prompts.controller.js | 92 ++++ .../prompt-other-prompts.directive.js | 32 ++ .../prompt-other-prompts.partial.html | 110 +++++ .../preview/prompt-preview.controller.js | 85 ++++ .../steps/preview/prompt-preview.directive.js | 30 ++ .../steps/preview/prompt-preview.partial.html | 84 ++++ .../steps/survey/prompt-survey.controller.js | 35 ++ .../steps/survey/prompt-survey.directive.js | 31 ++ .../steps/survey/prompt-survey.partial.html | 65 +++ 55 files changed, 2814 insertions(+), 607 deletions(-) create mode 100644 awx/ui/client/lib/models/Job.js create mode 100644 awx/ui/client/lib/models/WorkflowJob.js delete mode 100644 awx/ui/client/src/scheduler/factories/add-schedule.factory.js delete mode 100644 awx/ui/client/src/scheduler/factories/edit-schedule.factory.js create mode 100644 awx/ui/client/src/templates/prompt/main.js create mode 100644 awx/ui/client/src/templates/prompt/prompt.block.less create mode 100644 awx/ui/client/src/templates/prompt/prompt.controller.js create mode 100644 awx/ui/client/src/templates/prompt/prompt.directive.js create mode 100644 awx/ui/client/src/templates/prompt/prompt.partial.html create mode 100644 awx/ui/client/src/templates/prompt/prompt.service.js create mode 100644 awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js create mode 100644 awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js create mode 100644 awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html create mode 100644 awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js create mode 100644 awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js create mode 100644 awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html create mode 100644 awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js create mode 100644 awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js create mode 100644 awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html create mode 100644 awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.controller.js create mode 100644 awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.directive.js create mode 100644 awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html create mode 100644 awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.controller.js create mode 100644 awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js create mode 100644 awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html diff --git a/awx/ui/client/features/credentials/add-edit-credentials.view.html b/awx/ui/client/features/credentials/add-edit-credentials.view.html index 2019f403f1..182eadf004 100644 --- a/awx/ui/client/features/credentials/add-edit-credentials.view.html +++ b/awx/ui/client/features/credentials/add-edit-credentials.view.html @@ -42,4 +42,3 @@
- diff --git a/awx/ui/client/features/templates/list-templates.controller.js b/awx/ui/client/features/templates/list-templates.controller.js index 336c3a1cb0..1f6e156e4a 100644 --- a/awx/ui/client/features/templates/list-templates.controller.js +++ b/awx/ui/client/features/templates/list-templates.controller.js @@ -1,4 +1,4 @@ -function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strings, $state, $scope, rbacUiControlService, Dataset, $filter, Alert, InitiatePlaybookRun, Prompt, Wait, ProcessErrors, TemplateCopyService) { +function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strings, $state, $scope, rbacUiControlService, Dataset, $filter, Alert, InitiatePlaybookRun, Prompt, Wait, ProcessErrors, TemplateCopyService, $q, Empty, i18n, PromptService) { const vm = this || {}, unifiedJobTemplate = model, jobTemplate = new JobTemplate(), @@ -85,28 +85,157 @@ function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strin // TODO: edit indicator doesn't update when you enter edit route after initial load right now vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id); - // TODO: update to new way of launching job after mike opens his pr vm.submitJob = 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' }); + let jobTemplate = new JobTemplate(); + + $q.all([jobTemplate.optionsLaunch(template.id), jobTemplate.getLaunch(template.id)]) + .then((responses) => { + if(jobTemplate.canLaunchWithoutPrompt()) { + jobTemplate.postLaunch({id: template.id}) + .then((launchRes) => { + $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); + }); + } else { + + if(responses[1].data.survey_enabled) { + + // go out and get the survey questions + jobTemplate.getSurveyQuestions(template.id) + .then((surveyQuestionRes) => { + + let processed = PromptService.processSurveyQuestions({ + surveyQuestions: surveyQuestionRes.data.spec + }); + + $scope.promptData = { + launchConf: responses[1].data, + launchOptions: responses[0].data, + surveyQuestions: processed.surveyQuestions, + template: template.id, + prompts: PromptService.processPromptValues({ + launchConf: responses[1].data, + launchOptions: responses[0].data + }), + triggerModalOpen: true + }; + }); + } + else { + $scope.promptData = { + launchConf: responses[1].data, + launchOptions: responses[0].data, + template: template.id, + prompts: PromptService.processPromptValues({ + launchConf: responses[1].data, + launchOptions: responses[0].data + }), + triggerModalOpen: true + }; + } + } + }); } 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 { - var alertStrings = { - header: 'Error: Unable to determine template type', - body: 'We were unable to determine this template\'s type while launching.' + // 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.launchJob = () => { + + let jobLaunchData = { + extra_vars: $scope.promptData.extraVars + }; + + let jobTemplate = new JobTemplate(); + + 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 = {}; } - Alert(strings.get('ALERT', alertStrings)); + 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.postLaunch({ + id: $scope.promptData.template, + launchData: jobLaunchData + }).then((launchRes) => { + $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); + }).catch(({data, status}) => { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Failed to launch job template. POST returned: %d'), $scope.promptData.template, status) }); + }); + }; + + vm.scheduleJob = (template) => { + if(template) { + if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) { + $state.go('jobTemplateSchedules', {id: template.id}); } - } else { - var alertStrings = { - header: 'Error: Unable to launch template', - body: 'Template parameter is missing' + else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) { + $state.go('workflowJobTemplateSchedules', {id: template.id}); } - Alert(strings.get('ALERT', alertStrings)); + else { + // Something went wrong Let the user know that we're unable to redirect to schedule 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 routing to schedule.'); + } + } + else { + Alert('Error: Unable to schedule job', 'Template parameter is missing'); } }; @@ -334,7 +463,11 @@ ListTemplatesController.$inject = [ 'Prompt', 'Wait', 'ProcessErrors', - 'TemplateCopyService' + 'TemplateCopyService', + '$q', + 'Empty', + 'i18n', + 'PromptService' ]; export default ListTemplatesController; diff --git a/awx/ui/client/features/templates/list.view.html b/awx/ui/client/features/templates/list.view.html index c1d8e030dc..d22d64cfb4 100644 --- a/awx/ui/client/features/templates/list.view.html +++ b/awx/ui/client/features/templates/list.view.html @@ -118,4 +118,5 @@ query-set="querySet"> + diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index 0c60c10568..d0cef0df54 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -6,7 +6,7 @@ function TemplatesStrings (BaseString) { ns.state = { LIST_BREADCRUMB_LABEL: t.s('TEMPLATES') - } + }; ns.list = { PANEL_TITLE: t.s('TEMPLATES'), @@ -19,7 +19,44 @@ function TemplatesStrings (BaseString) { ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'), ROW_ITEM_LABEL_MODIFIED: t.s('Last Modified'), ROW_ITEM_LABEL_RAN: t.s('Last Ran'), - } + }; + + ns.prompt = { + INVENTORY: t.s('Inventory'), + CREDENTIAL: t.s('Credential'), + OTHER_PROMPTS: t.s('Other Prompts'), + SURVEY: t.s('Survey'), + PREVIEW: t.s('Preview'), + LAUNCH: t.s('LAUNCH'), + SELECTED: t.s('SELECTED'), + NO_CREDENTIALS_SELECTED: t.s('No credentials selected'), + NO_INVENTORY_SELECTED: t.s('No inventory selected'), + REVERT: t.s('REVERT'), + CREDENTIAL_TYPE: t.s('Credential Type'), + PASSWORDS_REQUIRED_HELP: t.s('Launching this job requires the passwords listed below. Enter and confirm each password before continuing.'), + PLEASE_ENTER_PASSWORD: t.s('Please enter a password.'), + credential_passwords: { + SSH_PASSWORD: t.s('SSH Password'), + PRIVATE_KEY_PASSPHRASE: t.s('Private Key Passphrase'), + PRIVILEGE_ESCALATION_PASSWORD: t.s('Privilege Escalation Password'), + VAULT_PASSWORD: t.s('Vault Password') + }, + SHOW_CHANGES: t.s('Show Changes'), + SKIP_TAGS: t.s('Skip Tags'), + JOB_TAGS: t.s('Job Tags'), + LIMIT: t.s('Limit'), + JOB_TYPE: t.s('Job Type'), + VERBOSITY: t.s('Verbosity'), + CHOOSE_JOB_TYPE: t.s('Choose a job type'), + CHOOSE_VERBOSITY: t.s('Choose a verbosity'), + EXTRA_VARIABLES: t.s('Extra Variables'), + PLEASE_ENTER_ANSWER: t.s('Please enter an answer.'), + VALID_INTEGER: t.s('Please enter an answer that is a valid integer.'), + VALID_DECIMAL: t.s('Please enter an answer that is a decimal number.'), + PLAYBOOK_RUN: t.s('Playbook Run'), + CHECK: t.s('Check'), + NO_CREDS_MATCHING_TYPE: t.s('No Credentials Matching This Type Have Been Created') + }; } TemplatesStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/legacy/styles/forms.less b/awx/ui/client/legacy/styles/forms.less index b72821e8e3..9cc5779d36 100644 --- a/awx/ui/client/legacy/styles/forms.less +++ b/awx/ui/client/legacy/styles/forms.less @@ -626,11 +626,20 @@ input[type='radio']:checked:before { background-color: @submit-button-bg-dis; } +.Form-saveButton--disabled { + background-color: @submit-button-bg-dis; + cursor: not-allowed; +} + .Form-saveButton:hover, .Form-launchButton:hover { background-color: @submit-button-bg-hov; color: @submit-button-text; } +.Form-saveButton--disabled:hover { + background-color: @submit-button-bg-dis; +} + .Form-cancelButton { background-color: @default-bg; color: @btn-txt; @@ -668,6 +677,10 @@ input[type='radio']:checked:before { background-color: @default-link; } +.Form-primaryButton--noMargin { + margin-right: 0px; +} + .Form-formGroup--singleColumn { width: 100% !important; padding-right: 0px; diff --git a/awx/ui/client/lib/components/modal/modal.directive.js b/awx/ui/client/lib/components/modal/modal.directive.js index e1d1126121..302ff92a03 100644 --- a/awx/ui/client/lib/components/modal/modal.directive.js +++ b/awx/ui/client/lib/components/modal/modal.directive.js @@ -16,14 +16,12 @@ function AtModalController (eventService, strings) { const vm = this; let overlay; - let modal; let listeners; vm.strings = strings; vm.init = (scope, el) => { overlay = el[0]; // eslint-disable-line prefer-destructuring - modal = el.find('.at-Modal-window')[0]; // eslint-disable-line prefer-destructuring vm.modal = scope[scope.ns].modal; vm.modal.show = vm.show; @@ -35,7 +33,7 @@ function AtModalController (eventService, strings) { vm.modal.message = message; listeners = eventService.addListeners([ - [window, 'click', vm.clickToHide] + [overlay, 'click', vm.clickToHide] ]); overlay.style.display = 'block'; @@ -53,22 +51,10 @@ function AtModalController (eventService, strings) { }; vm.clickToHide = event => { - if (vm.clickIsOutsideModal(event)) { + if ($(event.target).hasClass('at-Modal')) { vm.hide(); } }; - - vm.clickIsOutsideModal = e => { - const m = modal.getBoundingClientRect(); - const cx = e.clientX; - const cy = e.clientY; - - if (cx < m.left || cx > m.right || cy > m.bottom || cy < m.top) { - return true; - } - - return false; - }; } AtModalController.$inject = [ diff --git a/awx/ui/client/lib/components/tabs/group.directive.js b/awx/ui/client/lib/components/tabs/group.directive.js index 4d27d2e03d..d3c55a295f 100644 --- a/awx/ui/client/lib/components/tabs/group.directive.js +++ b/awx/ui/client/lib/components/tabs/group.directive.js @@ -10,6 +10,12 @@ function AtTabGroupController () { vm.tabs.push(tab); }; + + vm.clearActive = () => { + vm.tabs.forEach((tab) => { + tab.state._active = false; + }); + }; } function atTabGroup () { diff --git a/awx/ui/client/lib/components/tabs/tab.directive.js b/awx/ui/client/lib/components/tabs/tab.directive.js index c70448c43c..4c7eea3785 100644 --- a/awx/ui/client/lib/components/tabs/tab.directive.js +++ b/awx/ui/client/lib/components/tabs/tab.directive.js @@ -25,7 +25,12 @@ function AtTabController ($state) { return; } - $state.go(scope.state._go, scope.state._params, { reload: true }); + if (scope.state._go) { + $state.go(scope.state._go, scope.state._params, { reload: true }); + } else { + group.clearActive(); + scope.state._active = true; + } }; } diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js new file mode 100644 index 0000000000..9be420b2f9 --- /dev/null +++ b/awx/ui/client/lib/models/Job.js @@ -0,0 +1,21 @@ +let Base; + +function JobModel (method, resource, config) { + Base.call(this, 'jobs'); + + this.Constructor = JobModel; + + return this.create(method, resource, config); +} + +function JobModelLoader (BaseModel) { + Base = BaseModel; + + return JobModel; +} + +JobModelLoader.$inject = [ + 'BaseModel' +]; + +export default JobModelLoader; diff --git a/awx/ui/client/lib/models/JobTemplate.js b/awx/ui/client/lib/models/JobTemplate.js index 885f1338a9..b1b3599f4b 100644 --- a/awx/ui/client/lib/models/JobTemplate.js +++ b/awx/ui/client/lib/models/JobTemplate.js @@ -1,5 +1,69 @@ let Base; let WorkflowJobTemplateNode; +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.ask_inventory_on_launch && + !launchData.ask_credential_on_launch && + !launchData.ask_verbosity_on_launch && + !launchData.ask_job_type_on_launch && + !launchData.ask_limit_on_launch && + !launchData.ask_tags_on_launch && + !launchData.ask_skip_tags_on_launch && + !launchData.ask_variables_on_launch && + !launchData.ask_diff_mode_on_launch && + !launchData.survey_enabled + ); +} function setDependentResources (id) { this.dependentResources = [ @@ -17,20 +81,30 @@ function JobTemplateModel (method, resource, config) { this.Constructor = JobTemplateModel; this.setDependentResources = setDependentResources.bind(this); + 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 JobTemplateModelLoader (BaseModel, WorkflowJobTemplateNodeModel) { +function JobTemplateModelLoader (BaseModel, WorkflowJobTemplateNodeModel, _$http_) { Base = BaseModel; WorkflowJobTemplateNode = WorkflowJobTemplateNodeModel; + $http = _$http_; return JobTemplateModel; } JobTemplateModelLoader.$inject = [ 'BaseModel', - 'WorkflowJobTemplateNodeModel' + 'WorkflowJobTemplateNodeModel', + '$http', + '$state' ]; export default JobTemplateModelLoader; diff --git a/awx/ui/client/lib/models/WorkflowJob.js b/awx/ui/client/lib/models/WorkflowJob.js new file mode 100644 index 0000000000..06ed451883 --- /dev/null +++ b/awx/ui/client/lib/models/WorkflowJob.js @@ -0,0 +1,21 @@ +let Base; + +function WorkflowJobModel (method, resource, config) { + Base.call(this, 'workflow_jobs'); + + this.Constructor = WorkflowJobModel; + + return this.create(method, resource, config); +} + +function WorkflowJobModelLoader (BaseModel) { + Base = BaseModel; + + return WorkflowJobModel; +} + +WorkflowJobModelLoader.$inject = [ + 'BaseModel' +]; + +export default WorkflowJobModelLoader; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 6dbea4c954..937d97ac11 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -12,6 +12,8 @@ import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode'; import InventorySource from '~models/InventorySource'; import Inventory from '~models/Inventory'; import InventoryScript from '~models/InventoryScript'; +import Job from '~models/Job'; +import WorkflowJob from '~models/WorkflowJob'; import WorkflowJobTemplate from '~models/WorkflowJobTemplate'; import ModelsStrings from '~models/models.strings'; @@ -37,6 +39,9 @@ angular .service('InventoryScriptModel', InventoryScript) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) .service('ModelsStrings', ModelsStrings) - .service('UnifiedJobTemplateModel', UnifiedJobTemplate); + .service('UnifiedJobTemplateModel', UnifiedJobTemplate) + .service('JobModel', Job) + .service('WorkflowJobModel', WorkflowJob) + .service('WorkflowJobTemplateModel', WorkflowJobTemplate); export default MODULE_NAME; diff --git a/awx/ui/client/lib/services/base-string.service.js b/awx/ui/client/lib/services/base-string.service.js index a14871ae68..30762fb8db 100644 --- a/awx/ui/client/lib/services/base-string.service.js +++ b/awx/ui/client/lib/services/base-string.service.js @@ -60,6 +60,13 @@ function BaseStringService (namespace) { this.CANCEL = t.s('CANCEL'); this.SAVE = t.s('SAVE'); this.OK = t.s('OK'); + this.NEXT = t.s('NEXT'); + this.SHOW = t.s('SHOW'); + this.HIDE = t.s('HIDE'); + this.ON = t.s('ON'); + this.OFF = t.s('OFF'); + this.YAML = t.s('YAML'); + this.JSON = t.s('JSON'); this.deleteResource = { HEADER: t.s('Delete'), USED_BY: resourceType => t.s('The {{ resourceType }} is currently being used by other resources.', { resourceType }), diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less index 1f0d3dd254..416c4a3f8d 100644 --- a/awx/ui/client/lib/theme/index.less +++ b/awx/ui/client/lib/theme/index.less @@ -120,6 +120,7 @@ @import '../../src/system-tracking/fact-module-filter.block.less'; @import '../../src/system-tracking/fact-module-pickers.block.less'; @import '../../src/system-tracking/system-tracking-container.block.less'; +@import '../../src/templates/prompt/prompt.block.less'; @import '../../src/templates/job_templates/multi-credential/multi-credential.block.less'; @import '../../src/templates/labels/labelsList.block.less'; @import '../../src/templates/survey-maker/survey-maker.block.less'; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-groups-labels/relatedGroupsLabelsList.partial.html b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-groups-labels/relatedGroupsLabelsList.partial.html index d8e42dfc01..8b40988fdd 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-groups-labels/relatedGroupsLabelsList.partial.html +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-groups-labels/relatedGroupsLabelsList.partial.html @@ -1,12 +1,12 @@
+
+ {{ related_group.name }} +
-
- {{ related_group.name }} -
View More
diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 36cb42a7ed..60744ef069 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -394,20 +394,9 @@ export default } }; - $scope.toggle_inventory = function(id) { - $scope.inventories.forEach(function(row, i) { - if (row.id === id) { - $scope.selected_inventory = angular.copy(row); - $scope.inventories[i].checked = 1; - } else { - $scope.inventories[i].checked = 0; - } - }); - }; - - $scope.toggle_credential = function(id) { + $scope.toggle_credential = function(cred) { $scope.credentials.forEach(function(row, i) { - if (row.id === id) { + if (row.id === cred.id) { $scope.selected_credentials.machine = angular.copy(row); $scope.credentials[i].checked = 1; } else { diff --git a/awx/ui/client/src/job-submission/job-submission.partial.html b/awx/ui/client/src/job-submission/job-submission.partial.html index 75f3951ae3..dcf03bf6cb 100644 --- a/awx/ui/client/src/job-submission/job-submission.partial.html +++ b/awx/ui/client/src/job-submission/job-submission.partial.html @@ -31,12 +31,12 @@
-
- -
{{selected_inventory.name}}
+
+ +
@@ -58,22 +58,22 @@
-
- -
MACHINE: {{selected_credentials.machine.name}}
-
-
-
+
+
+
{{credential_types[extraCredential.credential_type].name | uppercase}}: {{extraCredential.name}}
+
+ +
diff --git a/awx/ui/client/src/scheduler/factories/add-schedule.factory.js b/awx/ui/client/src/scheduler/factories/add-schedule.factory.js deleted file mode 100644 index c1870dd469..0000000000 --- a/awx/ui/client/src/scheduler/factories/add-schedule.factory.js +++ /dev/null @@ -1,135 +0,0 @@ -export default - function AddSchedule($location, $rootScope, $stateParams, SchedulerInit, - Wait, GetBasePath, Empty, SchedulePost, $state, Rest, - ProcessErrors) { - return function(params) { - var scope = params.scope, - callback= params.callback, - base = params.base || $location.path().replace(/^\//, '').split('/')[0], - url = params.url || null, - scheduler, - job_type; - - job_type = scope.parentObject.job_type; - if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !url) { - url = GetBasePath(base) + $stateParams.id + '/schedules/'; - } - else if(base === "inventories"){ - if (!params.url){ - url = GetBasePath('groups') + $stateParams.id + '/'; - Rest.setUrl(url); - Rest.get(). - then(function (data) { - url = data.data.related.inventory_source + 'schedules/'; - }).catch(function (response) { - ProcessErrors(null, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get inventory group info. GET returned status: ' + - response.status - }); - }); - } - else { - url = params.url; - } - } - else if (base === 'system_job_templates') { - url = GetBasePath(base) + $stateParams.id + '/schedules/'; - if(job_type === "cleanup_facts"){ - scope.isFactCleanup = true; - scope.keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.granularity_keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.prompt_for_days_facts_form.keep_amount.$setViewValue(30); - scope.prompt_for_days_facts_form.granularity_keep_amount.$setViewValue(1); - scope.keep_unit = scope.keep_unit_choices[0]; - scope.granularity_keep_unit = scope.granularity_keep_unit_choices[1]; - } - else { - scope.cleanupJob = true; - } - } - - Wait('start'); - $('#form-container').empty(); - scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); - if(scope.schedulerUTCTime) { - // The UTC time is already set - scope.processSchedulerEndDt(); - } - else { - // We need to wait for it to be set by angular-scheduler because the following function depends - // on it - var schedulerUTCTimeWatcher = scope.$watch('schedulerUTCTime', function(newVal) { - if(newVal) { - // Remove the watcher - schedulerUTCTimeWatcher(); - scope.processSchedulerEndDt(); - } - }); - } - scheduler.inject('form-container', false); - scheduler.injectDetail('occurrences', false); - scheduler.clear(); - scope.$on("htmlDetailReady", function() { - $rootScope.$broadcast("ScheduleFormCreated", scope); - }); - scope.showRRuleDetail = false; - - if (scope.removeScheduleSaved) { - scope.removeScheduleSaved(); - } - scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { - Wait('stop'); - if (callback) { - scope.$emit(callback, data); - } - $state.go("^", null, {reload: true}); - }); - scope.saveSchedule = function() { - SchedulePost({ - scope: scope, - url: url, - scheduler: scheduler, - callback: 'ScheduleSaved', - mode: 'add' - }); - }; - - $('#scheduler-tabs li a').on('shown.bs.tab', function(e) { - if ($(e.target).text() === 'Details') { - if (!scheduler.isValid()) { - $('#scheduler-tabs a:first').tab('show'); - } - } - }); - }; - } - -AddSchedule.$inject = - [ '$location', '$rootScope', '$stateParams', - 'SchedulerInit', 'Wait', 'GetBasePath', - 'Empty', 'SchedulePost', '$state', - 'Rest', 'ProcessErrors' - ]; diff --git a/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js b/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js deleted file mode 100644 index fe8c2eff24..0000000000 --- a/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js +++ /dev/null @@ -1,154 +0,0 @@ -export default - function EditSchedule(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors, - GetBasePath, SchedulePost, $state) { - return function(params) { - var scope = params.scope, - id = params.id, - callback = params.callback, - schedule, scheduler, - url = GetBasePath('schedules') + id + '/'; - - delete scope.isFactCleanup; - delete scope.cleanupJob; - - function setGranularity(){ - var a,b, prompt_for_days, - keep_unit, - granularity, - granularity_keep_unit; - - if(scope.cleanupJob){ - scope.schedulerPurgeDays = Number(schedule.extra_data.days); - // scope.scheduler_form.schedulerPurgeDays.$setViewValue( Number(schedule.extra_data.days)); - } - else if(scope.isFactCleanup){ - scope.keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.granularity_keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - // the API returns something like 20w or 1y - a = schedule.extra_data.older_than; // "20y" - b = schedule.extra_data.granularity; // "1w" - prompt_for_days = Number(_.initial(a,1).join('')); // 20 - keep_unit = _.last(a); // "y" - granularity = Number(_.initial(b,1).join('')); // 1 - granularity_keep_unit = _.last(b); // "w" - - scope.keep_amount = prompt_for_days; - scope.granularity_keep_amount = granularity; - scope.keep_unit = _.find(scope.keep_unit_choices, function(i){ - return i.value === keep_unit; - }); - scope.granularity_keep_unit =_.find(scope.granularity_keep_unit_choices, function(i){ - return i.value === granularity_keep_unit; - }); - } - } - - if (scope.removeScheduleFound) { - scope.removeScheduleFound(); - } - scope.removeScheduleFound = scope.$on('ScheduleFound', function() { - $('#form-container').empty(); - scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); - scheduler.inject('form-container', false); - scheduler.injectDetail('occurrences', false); - - if (!/DTSTART/.test(schedule.rrule)) { - schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); - } - schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); - schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); - scope.$on("htmlDetailReady", function() { - scheduler.setRRule(schedule.rrule); - scheduler.setName(schedule.name); - $rootScope.$broadcast("ScheduleFormCreated", scope); - }); - scope.showRRuleDetail = false; - - scheduler.setRRule(schedule.rrule); - scheduler.setName(schedule.name); - if(scope.isFactCleanup || scope.cleanupJob){ - setGranularity(); - } - }); - - - if (scope.removeScheduleSaved) { - scope.removeScheduleSaved(); - } - scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { - Wait('stop'); - if (callback) { - scope.$emit(callback, data); - } - $state.go("^"); - }); - scope.saveSchedule = function() { - schedule.extra_data = scope.extraVars; - SchedulePost({ - scope: scope, - url: url, - scheduler: scheduler, - callback: 'ScheduleSaved', - mode: 'edit', - schedule: schedule - }); - }; - - Wait('start'); - - // Get the existing record - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - schedule = data; - try { - schedule.extra_data = JSON.parse(schedule.extra_data); - } catch(e) { - // do nothing - } - scope.extraVars = data.extra_data === '' ? '---' : '---\n' + jsyaml.safeDump(data.extra_data); - - if(schedule.extra_data.hasOwnProperty('granularity')){ - scope.isFactCleanup = true; - } - if (schedule.extra_data.hasOwnProperty('days')){ - scope.cleanupJob = true; - } - - scope.schedule_obj = data; - - scope.$emit('ScheduleFound'); - }) - .catch(({data, status}) => { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); - }); - }; - } - -EditSchedule.$inject = - [ 'SchedulerInit', '$rootScope', 'Wait', 'Rest', - 'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state' - ]; diff --git a/awx/ui/client/src/scheduler/factories/schedule-post.factory.js b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js index e62388368e..3033219275 100644 --- a/awx/ui/client/src/scheduler/factories/schedule-post.factory.js +++ b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js @@ -1,13 +1,14 @@ export default - function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait) { + function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait, $q) { return function(params) { var scope = params.scope, url = params.url, scheduler = params.scheduler, mode = params.mode, schedule = (params.schedule) ? params.schedule : {}, - callback = params.callback, + promptData = params.promptData, newSchedule, rrule, extra_vars; + let deferred = $q.defer(); if (scheduler.isValid()) { Wait('start'); newSchedule = scheduler.getValue(); @@ -32,41 +33,101 @@ export default schedule.extra_data = scope.parseType === 'yaml' ? (scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars; } + + if(promptData) { + if(promptData.launchConf.survey_enabled){ + for (var i=0; i < promptData.surveyQuestions.length; i++){ + var fld = promptData.surveyQuestions[i].variable; + // grab all survey questions that have answers + if(promptData.surveyQuestions[i].required || (promptData.surveyQuestions[i].required === false && promptData.surveyQuestions[i].model.toString()!=="")) { + if(!schedule.extra_data) { + schedule.extra_data = {}; + } + schedule.extra_data[fld] = promptData.surveyQuestions[i].model; + } + + if(promptData.surveyQuestions[i].required === false && _.isEmpty(promptData.surveyQuestions[i].model)) { + switch (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 (promptData.surveyQuestions[i].min === 0) { + schedule.extra_data[fld] = ""; + } + break; + } + } + } + } + + if(_.has(promptData, 'prompts.jobType.value.value') && _.get(promptData, 'launchConf.ask_job_type_on_launch')) { + schedule.job_type = promptData.prompts.jobType.templateDefault === promptData.prompts.jobType.value.value ? null : promptData.prompts.jobType.value.value; + } + if(_.has(promptData, 'prompts.tags.value') && _.get(promptData, 'launchConf.ask_tags_on_launch')){ + let templateDefaultJobTags = promptData.prompts.tags.templateDefault.split(','); + schedule.job_tags = (_.isEqual(templateDefaultJobTags.sort(), promptData.prompts.tags.value.map(a => a.value).sort())) ? null : promptData.prompts.tags.value.map(a => a.value).join(); + } + if(_.has(promptData, 'prompts.skipTags.value') && _.get(promptData, 'launchConf.ask_skip_tags_on_launch')){ + let templateDefaultSkipTags = promptData.prompts.skipTags.templateDefault.split(','); + schedule.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : promptData.prompts.skipTags.value.map(a => a.value).join(); + } + if(_.has(promptData, 'prompts.limit.value') && _.get(promptData, 'launchConf.ask_limit_on_launch')){ + schedule.limit = promptData.prompts.limit.templateDefault === promptData.prompts.limit.value ? null : promptData.prompts.limit.value; + } + if(_.has(promptData, 'prompts.verbosity.value.value') && _.get(promptData, 'launchConf.ask_verbosity_on_launch')){ + schedule.verbosity = promptData.prompts.verbosity.templateDefault === promptData.prompts.verbosity.value.value ? null : promptData.prompts.verbosity.value.value; + } + if(_.has(promptData, 'prompts.inventory.value') && _.get(promptData, 'launchConf.ask_inventory_on_launch')){ + schedule.inventory = promptData.prompts.inventory.templateDefault.id === promptData.prompts.inventory.value.id ? null : promptData.prompts.inventory.value.id; + } + if(_.has(promptData, 'prompts.diffMode.value') && _.get(promptData, 'launchConf.ask_diff_mode_on_launch')){ + schedule.diff_mode = promptData.prompts.diffMode.templateDefault === promptData.prompts.diffMode.value ? null : promptData.prompts.diffMode.value; + } + // Credentials gets POST'd to a separate endpoint + // if($scope.promptData.launchConf.ask_credential_on_launch){ + // jobLaunchData.credentials = []; + // promptData.credentials.value.forEach((credential) => { + // jobLaunchData.credentials.push(credential.id); + // }); + // } + } + Rest.setUrl(url); if (mode === 'add') { Rest.post(schedule) .then(() => { - if (callback) { - scope.$emit(callback); - } - else { - Wait('stop'); - } + Wait('stop'); + deferred.resolve(); }) .catch(({data, status}) => { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'POST to ' + url + ' returned: ' + status }); + + deferred.reject(); }); } else { Rest.put(schedule) .then(() => { - if (callback) { - scope.$emit(callback, schedule); - } - else { - Wait('stop'); - } + Wait('stop'); + deferred.resolve(schedule); }) .catch(({data, status}) => { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'POST to ' + url + ' returned: ' + status }); + + deferred.reject(); }); } } else { - return false; + deferred.reject(); } + + return deferred.promise; }; } @@ -74,5 +135,6 @@ SchedulePost.$inject = [ 'Rest', 'ProcessErrors', 'RRuleToAPI', - 'Wait' + 'Wait', + '$q' ]; diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js index 6dbaf01971..7671614054 100644 --- a/awx/ui/client/src/scheduler/main.js +++ b/awx/ui/client/src/scheduler/main.js @@ -10,9 +10,7 @@ import editController from './schedulerEdit.controller'; import {templateUrl} from '../shared/template-url/template-url.factory'; import schedulerDatePicker from './schedulerDatePicker.directive'; import { N_ } from '../i18n'; -import AddSchedule from './factories/add-schedule.factory'; import DeleteSchedule from './factories/delete-schedule.factory'; -import EditSchedule from './factories/edit-schedule.factory'; import RRuleToAPI from './factories/r-rule-to-api.factory'; import SchedulePost from './factories/schedule-post.factory'; import ToggleSchedule from './factories/toggle-schedule.factory'; @@ -24,9 +22,7 @@ export default .controller('schedulerListController', listController) .controller('schedulerAddController', addController) .controller('schedulerEditController', editController) - .factory('AddSchedule', AddSchedule) .factory('DeleteSchedule', DeleteSchedule) - .factory('EditSchedule', EditSchedule) .factory('RRuleToAPI', RRuleToAPI) .factory('SchedulePost', SchedulePost) .factory('ToggleSchedule', ToggleSchedule) @@ -47,10 +43,10 @@ export default activityStreamTarget: 'job_template', activityStreamId: 'id' }, - ncyBreadcrumb: { - parent: 'templates.editJobTemplate({job_template_id: parentObject.id})', - label: N_('SCHEDULES') - }, + // ncyBreadcrumb: { + // parent: 'templates.editJobTemplate({job_template_id: parentObject.id})', + // label: N_('SCHEDULES') + // }, resolve: { Dataset: ['ScheduleList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { diff --git a/awx/ui/client/src/scheduler/schedulerAdd.controller.js b/awx/ui/client/src/scheduler/schedulerAdd.controller.js index 9485315078..d7f82c32ca 100644 --- a/awx/ui/client/src/scheduler/schedulerAdd.controller.js +++ b/awx/ui/client/src/scheduler/schedulerAdd.controller.js @@ -4,12 +4,22 @@ * All Rights Reserved *************************************************/ -export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait', +export default ['$filter', '$state', '$stateParams', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', 'GetBasePath', - 'Rest', 'ParentObject', - function($filter, $state, $stateParams, AddSchedule, Wait, $scope, - $rootScope, CreateSelect2, ParseTypeChange, GetBasePath, Rest, ParentObject) { - $scope.processSchedulerEndDt = function(){ + 'Rest', 'ParentObject', 'JobTemplateModel', '$q', 'Empty', 'SchedulePost', + 'ProcessErrors', 'SchedulerInit', '$location', 'PromptService', + function($filter, $state, $stateParams, Wait, + $scope, $rootScope, CreateSelect2, ParseTypeChange, GetBasePath, + Rest, ParentObject, JobTemplate, $q, Empty, SchedulePost, + ProcessErrors, SchedulerInit, $location, PromptService) { + + var base = $scope.base || $location.path().replace(/^\//, '').split('/')[0], + scheduler, + job_type; + + var schedule_url = ParentObject.related.schedules || `${ParentObject.related.inventory_source}schedules`; + + let processSchedulerEndDt = function(){ // set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight var dt = new Date($scope.schedulerUTCTime); // increment date by 1 day @@ -19,12 +29,6 @@ export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait', $scope.$parent.schedulerEndDt = month + '/' + day + '/' + dt.getFullYear(); }; - // initial end @ midnight values - $scope.schedulerEndHour = "00"; - $scope.schedulerEndMinute = "00"; - $scope.schedulerEndSecond = "00"; - $scope.parentObject = ParentObject; - /* * This is a workaround for the angular-scheduler library inserting `ll` into fields after an * invalid entry and never unsetting them. Presumably null is being truncated down to 2 chars @@ -50,9 +54,255 @@ export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait', $scope.scheduleTimeChange(); }; - $scope.$on("ScheduleFormCreated", function(e, scope) { + $scope.saveSchedule = function() { + SchedulePost({ + scope: $scope, + url: schedule_url, + scheduler: scheduler, + promptData: $scope.promptData, + mode: 'add' + }).then(() => { + Wait('stop'); + $state.go("^", null, {reload: true}); + }); + }; + + $scope.prompt = () => { + $scope.promptData.triggerModalOpen = true; + }; + + $scope.formCancel = function() { + $state.go("^"); + }; + + // initial end @ midnight values + $scope.schedulerEndHour = "00"; + $scope.schedulerEndMinute = "00"; + $scope.schedulerEndSecond = "00"; + $scope.parentObject = ParentObject; + + $scope.hideForm = true; + + // extra_data field is not manifested in the UI when scheduling a Management Job + if ($state.current.name === 'jobTemplateSchedules.add'){ + $scope.parseType = 'yaml'; + $scope.extraVars = '---'; + + ParseTypeChange({ + scope: $scope, + variable: 'extraVars', + parse_variable: 'parseType', + field_id: 'SchedulerForm-extraVars' + }); + + let jobTemplate = new JobTemplate(); + + $q.all([jobTemplate.optionsLaunch(ParentObject.id), jobTemplate.getLaunch(ParentObject.id)]) + .then((responses) => { + let launchConf = responses[1].data; + + let watchForPromptChanges = () => { + let promptValuesToWatch = [ + 'promptData.prompts.inventory.value', + 'promptData.prompts.verbosity.value', + 'missingSurveyValue' + ]; + + $scope.$watchGroup(promptValuesToWatch, function() { + let missingPromptValue = false; + if($scope.missingSurveyValue) { + missingPromptValue = true; + } else if(!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) { + missingPromptValue = true; + } + $scope.promptModalMissingReqFields = missingPromptValue; + }); + }; + + if(!launchConf.ask_variables_on_launch) { + $scope.noVars = true; + } + + if(!launchConf.survey_enabled && + !launchConf.ask_inventory_on_launch && + !launchConf.ask_credential_on_launch && + !launchConf.ask_verbosity_on_launch && + !launchConf.ask_job_type_on_launch && + !launchConf.ask_limit_on_launch && + !launchConf.ask_tags_on_launch && + !launchConf.ask_skip_tags_on_launch && + !launchConf.ask_diff_mode_on_launch && + !launchConf.survey_enabled && + !launchConf.credential_needed_to_start && + !launchConf.inventory_needed_to_start && + launchConf.passwords_needed_to_start.length === 0 && + launchConf.variables_needed_to_start.length === 0) { + $scope.showPromptButton = false; + } else { + $scope.showPromptButton = true; + + // Ignore the fact that variables might be promptable on launch + // Promptable variables will happen in the schedule form + launchConf.ignore_ask_variables = true; + + if(launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) { + $scope.promptModalMissingReqFields = true; + } + + if(launchConf.survey_enabled) { + // go out and get the survey questions + jobTemplate.getSurveyQuestions(ParentObject.id) + .then((surveyQuestionRes) => { + + let processed = PromptService.processSurveyQuestions({ + surveyQuestions: surveyQuestionRes.data.spec + }); + + $scope.missingSurveyValue = processed.missingSurveyValue; + + $scope.promptData = { + launchConf: responses[1].data, + launchOptions: responses[0].data, + surveyQuestions: processed.surveyQuestions, + template: ParentObject.id, + prompts: PromptService.processPromptValues({ + launchConf: responses[1].data, + launchOptions: responses[0].data + }), + }; + + $scope.$watch('promptData.surveyQuestions', () => { + let missingSurveyValue = false; + _.each($scope.promptData.surveyQuestions, (question) => { + if(question.required && (Empty(question.model) || question.model === [])) { + missingSurveyValue = true; + } + }); + $scope.missingSurveyValue = missingSurveyValue; + }, true); + + watchForPromptChanges(); + }); + } + else { + $scope.promptData = { + launchConf: responses[1].data, + launchOptions: responses[0].data, + template: ParentObject.id, + prompts: PromptService.processPromptValues({ + launchConf: responses[1].data, + launchOptions: responses[0].data + }), + }; + + watchForPromptChanges(); + } + } + }); + } + else if ($state.current.name === 'workflowJobTemplateSchedules.add'){ + $scope.parseType = 'yaml'; + // grab any existing extra_vars from parent workflow_job_template + let defaultUrl = GetBasePath('workflow_job_templates') + $stateParams.id + '/'; + Rest.setUrl(defaultUrl); + Rest.get().then(function(res){ + var data = res.data.extra_vars; + $scope.extraVars = data === '' ? '---' : data; + ParseTypeChange({ + scope: $scope, + variable: 'extraVars', + parse_variable: 'parseType', + field_id: 'SchedulerForm-extraVars' + }); + }); + } + else if ($state.current.name === 'projectSchedules.add'){ + $scope.noVars = true; + } + else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.add'){ + $scope.noVars = true; + } + + job_type = $scope.parentObject.job_type; + if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !schedule_url) { + schedule_url = GetBasePath(base) + $stateParams.id + '/schedules/'; + } + else if(base === "inventories"){ + if (!schedule_url){ + Rest.setUrl(GetBasePath('groups') + $stateParams.id + '/'); + Rest.get() + .then(function (data) { + schedule_url = data.data.related.inventory_source + 'schedules/'; + }).catch(function (response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get inventory group info. GET returned status: ' + + response.status + }); + }); + } + } + else if (base === 'system_job_templates') { + schedule_url = GetBasePath(base) + $stateParams.id + '/schedules/'; + if(job_type === "cleanup_facts"){ + $scope.isFactCleanup = true; + $scope.keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + $scope.granularity_keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + $scope.prompt_for_days_facts_form.keep_amount.$setViewValue(30); + $scope.prompt_for_days_facts_form.granularity_keep_amount.$setViewValue(1); + $scope.keep_unit = $scope.keep_unit_choices[0]; + $scope.granularity_keep_unit = $scope.granularity_keep_unit_choices[1]; + } + else { + $scope.cleanupJob = true; + } + } + + Wait('start'); + $('#form-container').empty(); + scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false }); + if($scope.schedulerUTCTime) { + // The UTC time is already set + processSchedulerEndDt(); + } + else { + // We need to wait for it to be set by angular-scheduler because the following function depends + // on it + var schedulerUTCTimeWatcher = $scope.$watch('schedulerUTCTime', function(newVal) { + if(newVal) { + // Remove the watcher + schedulerUTCTimeWatcher(); + processSchedulerEndDt(); + } + }); + } + scheduler.inject('form-container', false); + scheduler.injectDetail('occurrences', false); + scheduler.clear(); + $scope.$on("htmlDetailReady", function() { $scope.hideForm = false; - $scope = angular.extend($scope, scope); $scope.$on("formUpdated", function() { $rootScope.$broadcast("loadSchedulerDetailPane"); }); @@ -91,72 +341,18 @@ export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait', Wait('stop'); }); + $scope.showRRuleDetail = false; - $scope.hideForm = true; - - var schedule_url = ParentObject.related.schedules || `${ParentObject.related.inventory_source}schedules`; - - $scope.formCancel = function() { - $state.go("^"); - }; - - // extra_data field is not manifested in the UI when scheduling a Management Job - if ($state.current.name === 'jobTemplateSchedules.add'){ - $scope.parseType = 'yaml'; - // grab any existing extra_vars from parent job_template - let defaultUrl = GetBasePath('job_templates') + $stateParams.id + '/'; - Rest.setUrl(defaultUrl); - Rest.get().then(function(res){ - var data = res.data.extra_vars; - $scope.extraVars = data === '' ? '---' : data; - ParseTypeChange({ - scope: $scope, - variable: 'extraVars', - parse_variable: 'parseType', - field_id: 'SchedulerForm-extraVars' - }); - }); - } - else if ($state.current.name === 'workflowJobTemplateSchedules.add'){ - $scope.parseType = 'yaml'; - // grab any existing extra_vars from parent workflow_job_template - let defaultUrl = GetBasePath('workflow_job_templates') + $stateParams.id + '/'; - Rest.setUrl(defaultUrl); - Rest.get().then(function(res){ - var data = res.data.extra_vars; - $scope.extraVars = data === '' ? '---' : data; - ParseTypeChange({ - scope: $scope, - variable: 'extraVars', - parse_variable: 'parseType', - field_id: 'SchedulerForm-extraVars' - }); - }); - } - else if ($state.current.name === 'projectSchedules.add'){ - $scope.noVars = true; - } - else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.add'){ - $scope.noVars = true; - } - AddSchedule({ - scope: $scope, - callback: 'SchedulesRefresh', - base: $scope.base ? $scope.base : null, - url: schedule_url + $('#scheduler-tabs li a').on('shown.bs.tab', function(e) { + if ($(e.target).text() === 'Details') { + if (!scheduler.isValid()) { + $('#scheduler-tabs a:first').tab('show'); + } + } }); - var callSelect2 = function() { - CreateSelect2({ - element: '.MakeSelect2', - multiple: false - }); - }; - - $scope.$on("updateSchedulerSelects", function() { - callSelect2(); + CreateSelect2({ + element: '.MakeSelect2', + multiple: false }); - - callSelect2(); - }]; diff --git a/awx/ui/client/src/scheduler/schedulerEdit.controller.js b/awx/ui/client/src/scheduler/schedulerEdit.controller.js index c6030fdd94..a762b06072 100644 --- a/awx/ui/client/src/scheduler/schedulerEdit.controller.js +++ b/awx/ui/client/src/scheduler/schedulerEdit.controller.js @@ -1,5 +1,20 @@ -export default ['$filter', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', 'ParentObject', -function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange, ParentObject) { +export default ['$filter', '$state', '$stateParams', 'Wait', '$scope', +'$rootScope', 'CreateSelect2', 'ParseTypeChange', 'ParentObject', 'ProcessErrors', 'Rest', +'GetBasePath', 'SchedulerInit', 'SchedulePost', 'JobTemplateModel', '$q', 'Empty', 'PromptService', +function($filter, $state, $stateParams, Wait, $scope, + $rootScope, CreateSelect2, ParseTypeChange, ParentObject, ProcessErrors, Rest, + GetBasePath, SchedulerInit, SchedulePost, JobTemplate, $q, Empty, PromptService) { + + let schedule, scheduler; + + // initial end @ midnight values + $scope.schedulerEndHour = "00"; + $scope.schedulerEndMinute = "00"; + $scope.schedulerEndSecond = "00"; + $scope.parentObject = ParentObject; + $scope.isEdit = true; + $scope.hideForm = true; + $scope.parseType = 'yaml'; $scope.processSchedulerEndDt = function(){ // set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight @@ -10,11 +25,10 @@ function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, day = $filter('schZeroPad')(dt.getDate(), 2); $scope.$parent.schedulerEndDt = month + '/' + day + '/' + dt.getFullYear(); }; - // initial end @ midnight values - $scope.schedulerEndHour = "00"; - $scope.schedulerEndMinute = "00"; - $scope.schedulerEndSecond = "00"; - $scope.parentObject = ParentObject; + + $scope.formCancel = function() { + $state.go("^"); + }; /* * This is a workaround for the angular-scheduler library inserting `ll` into fields after an @@ -41,85 +55,24 @@ function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, $scope.scheduleTimeChange(); }; - $scope.$on("ScheduleFormCreated", function(e, scope) { - $scope.hideForm = false; - $scope = angular.extend($scope, scope); - - $scope.$on("formUpdated", function() { - $rootScope.$broadcast("loadSchedulerDetailPane"); + $scope.saveSchedule = function() { + schedule.extra_data = $scope.extraVars; + SchedulePost({ + scope: $scope, + url: GetBasePath('schedules') + parseInt($stateParams.schedule_id) + '/', + scheduler: scheduler, + mode: 'edit', + schedule: schedule, + promptData: $scope.promptData + }).then(() => { + Wait('stop'); + $state.go("^", null, {reload: true}); }); - - $scope.$watchGroup(["schedulerName", - "schedulerStartDt", - "schedulerStartHour", - "schedulerStartMinute", - "schedulerStartSecond", - "schedulerTimeZone", - "schedulerFrequency", - "schedulerInterval", - "monthlyRepeatOption", - "monthDay", - "monthlyOccurrence", - "monthlyWeekDay", - "yearlyRepeatOption", - "yearlyMonth", - "yearlyMonthDay", - "yearlyOccurrence", - "yearlyWeekDay", - "yearlyOtherMonth", - "schedulerEnd", - "schedulerOccurrenceCount", - "schedulerEndDt" - ], function() { - $scope.$emit("formUpdated"); - }, true); - - $scope.$watch("weekDays", function() { - $scope.$emit("formUpdated"); - }, true); - - $rootScope.$broadcast("loadSchedulerDetailPane"); - Wait('stop'); - }); - - $scope.isEdit = true; - $scope.hideForm = true; - $scope.parseType = 'yaml'; - - $scope.formCancel = function() { - $state.go("^"); }; - // extra_data field is not manifested in the UI when scheduling a Management Job - if ($state.current.name !== 'managementJobsList.schedule.add' && $state.current.name !== 'managementJobsList.schedule.edit'){ - $scope.$on('ScheduleFound', function(){ - if ($state.current.name === 'projectSchedules.edit'){ - $scope.noVars = true; - } - else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.edit'){ - $scope.noVars = true; - } - else { - let readOnly = !$scope.schedule_obj.summary_fields.user_capabilities - .edit; - ParseTypeChange({ - scope: $scope, - variable: 'extraVars', - parse_variable: 'parseType', - field_id: 'SchedulerForm-extraVars', - readOnly: readOnly - }); - } - - }); - } - - EditSchedule({ - scope: $scope, - id: parseInt($stateParams.schedule_id), - callback: 'SchedulesRefresh', - base: $scope.base ? $scope.base: null - }); + $scope.prompt = () => { + $scope.promptData.triggerModalOpen = true; + }; var callSelect2 = function() { CreateSelect2({ @@ -132,5 +85,284 @@ function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, callSelect2(); }); + Wait('start'); + + // Get the existing record + Rest.setUrl(GetBasePath('schedules') + parseInt($stateParams.schedule_id) + '/'); + Rest.get() + .then(({data}) => { + schedule = data; + try { + schedule.extra_data = JSON.parse(schedule.extra_data); + } catch(e) { + // do nothing + } + + $scope.extraVars = (data.extra_data === '' || _.isEmpty(data.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(data.extra_data); + + if(schedule.extra_data.hasOwnProperty('granularity')){ + $scope.isFactCleanup = true; + } + if (schedule.extra_data.hasOwnProperty('days')){ + $scope.cleanupJob = true; + } + + $scope.schedule_obj = data; + + $('#form-container').empty(); + scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false }); + scheduler.inject('form-container', false); + scheduler.injectDetail('occurrences', false); + + if (!/DTSTART/.test(schedule.rrule)) { + schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); + } + schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); + schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); + $scope.$on("htmlDetailReady", function() { + scheduler.setRRule(schedule.rrule); + scheduler.setName(schedule.name); + $scope.hideForm = false; + + $scope.$watchGroup(["schedulerName", + "schedulerStartDt", + "schedulerStartHour", + "schedulerStartMinute", + "schedulerStartSecond", + "schedulerTimeZone", + "schedulerFrequency", + "schedulerInterval", + "monthlyRepeatOption", + "monthDay", + "monthlyOccurrence", + "monthlyWeekDay", + "yearlyRepeatOption", + "yearlyMonth", + "yearlyMonthDay", + "yearlyOccurrence", + "yearlyWeekDay", + "yearlyOtherMonth", + "schedulerEnd", + "schedulerOccurrenceCount", + "schedulerEndDt" + ], function() { + $rootScope.$broadcast("loadSchedulerDetailPane"); + }, true); + + $scope.$watch("weekDays", function() { + $rootScope.$broadcast("loadSchedulerDetailPane"); + }, true); + + $rootScope.$broadcast("loadSchedulerDetailPane"); + Wait('stop'); + }); + + $scope.showRRuleDetail = false; + scheduler.setRRule(schedule.rrule); + scheduler.setName(schedule.name); + + if($scope.isFactCleanup || $scope.cleanupJob){ + var a,b, prompt_for_days, + keep_unit, + granularity, + granularity_keep_unit; + + if($scope.cleanupJob){ + $scope.schedulerPurgeDays = Number(schedule.extra_data.days); + } + else if($scope.isFactCleanup){ + $scope.keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + $scope.granularity_keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + // the API returns something like 20w or 1y + a = schedule.extra_data.older_than; // "20y" + b = schedule.extra_data.granularity; // "1w" + prompt_for_days = Number(_.initial(a,1).join('')); // 20 + keep_unit = _.last(a); // "y" + granularity = Number(_.initial(b,1).join('')); // 1 + granularity_keep_unit = _.last(b); // "w" + + $scope.keep_amount = prompt_for_days; + $scope.granularity_keep_amount = granularity; + $scope.keep_unit = _.find($scope.keep_unit_choices, function(i){ + return i.value === keep_unit; + }); + $scope.granularity_keep_unit =_.find($scope.granularity_keep_unit_choices, function(i){ + return i.value === granularity_keep_unit; + }); + } + } + + if ($state.current.name === 'jobTemplateSchedules.edit'){ + + let jobTemplate = new JobTemplate(); + + Rest.setUrl(data.related.credentials); + + $q.all([jobTemplate.optionsLaunch(ParentObject.id), jobTemplate.getLaunch(ParentObject.id), Rest.get()]) + .then((responses) => { + let launchOptions = responses[0].data, + launchConf = responses[1].data, + scheduleCredentials = responses[2].data; + + + let watchForPromptChanges = () => { + let promptValuesToWatch = [ + // credential passwords...? + 'promptData.prompts.inventory.value', + 'promptData.prompts.verbosity.value', + 'missingSurveyValue' + ]; + + $scope.$watchGroup(promptValuesToWatch, function() { + let missingPromptValue = false; + if($scope.missingSurveyValue) { + missingPromptValue = true; + } else if(!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) { + missingPromptValue = true; + } + $scope.promptModalMissingReqFields = missingPromptValue; + }); + }; + + let prompts = PromptService.processPromptValues({ + launchConf: responses[1].data, + launchOptions: responses[0].data, + currentValues: data + }); + + prompts.credentials.value = scheduleCredentials.results.length > 0 ? scheduleCredentials.results : prompts.credentials.value; + + if(!launchConf.ask_variables_on_launch) { + $scope.noVars = true; + } + + if(!launchConf.survey_enabled && + !launchConf.ask_inventory_on_launch && + !launchConf.ask_credential_on_launch && + !launchConf.ask_verbosity_on_launch && + !launchConf.ask_job_type_on_launch && + !launchConf.ask_limit_on_launch && + !launchConf.ask_tags_on_launch && + !launchConf.ask_skip_tags_on_launch && + !launchConf.ask_diff_mode_on_launch && + !launchConf.survey_enabled && + !launchConf.credential_needed_to_start && + !launchConf.inventory_needed_to_start && + launchConf.passwords_needed_to_start.length === 0 && + launchConf.variables_needed_to_start.length === 0) { + $scope.showPromptButton = false; + } else { + $scope.showPromptButton = true; + + // Ignore the fact that variables might be promptable on launch + // Promptable variables will happen in the schedule form + launchConf.ignore_ask_variables = true; + + if(launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has(data, 'summary_fields.inventory')) { + $scope.promptModalMissingReqFields = true; + } + + if(responses[1].data.survey_enabled) { + // go out and get the survey questions + jobTemplate.getSurveyQuestions(ParentObject.id) + .then((surveyQuestionRes) => { + + let processed = PromptService.processSurveyQuestions({ + surveyQuestions: surveyQuestionRes.data.spec, + extra_data: data.extra_data + }); + + $scope.missingSurveyValue = processed.missingSurveyValue; + + $scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data); + + ParseTypeChange({ + scope: $scope, + variable: 'extraVars', + parse_variable: 'parseType', + field_id: 'SchedulerForm-extraVars', + readOnly: !$scope.schedule_obj.summary_fields.user_capabilities.edit + }); + + $scope.promptData = { + launchConf: launchConf, + launchOptions: launchOptions, + prompts: prompts, + surveyQuestions: surveyQuestionRes.data.spec, + template: ParentObject.id + }; + + $scope.$watch('promptData.surveyQuestions', () => { + let missingSurveyValue = false; + _.each($scope.promptData.surveyQuestions, (question) => { + if(question.required && (Empty(question.model) || question.model === [])) { + missingSurveyValue = true; + } + }); + $scope.missingSurveyValue = missingSurveyValue; + }, true); + + watchForPromptChanges(); + }); + } + else { + $scope.promptData = { + launchConf: launchConf, + launchOptions: launchOptions, + prompts: prompts, + template: ParentObject.id + }; + watchForPromptChanges(); + } + } + }); + } + + // extra_data field is not manifested in the UI when scheduling a Management Job + if ($state.current.name !== 'managementJobsList.schedule.add' && $state.current.name !== 'managementJobsList.schedule.edit'){ + if ($state.current.name === 'projectSchedules.edit'){ + $scope.noVars = true; + } + else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.edit'){ + $scope.noVars = true; + } + else { + ParseTypeChange({ + scope: $scope, + variable: 'extraVars', + parse_variable: 'parseType', + field_id: 'SchedulerForm-extraVars', + readOnly: !$scope.schedule_obj.summary_fields.user_capabilities.edit + }); + } + } + }) + .catch(({data, status}) => { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve schedule ' + parseInt($stateParams.schedule_id) + ' GET returned: ' + status }); + }); + callSelect2(); }]; diff --git a/awx/ui/client/src/scheduler/schedulerForm.block.less b/awx/ui/client/src/scheduler/schedulerForm.block.less index 6512efa0f6..d40eb0e15e 100644 --- a/awx/ui/client/src/scheduler/schedulerForm.block.less +++ b/awx/ui/client/src/scheduler/schedulerForm.block.less @@ -54,3 +54,15 @@ margin-right: 0px; } } + +.SchedulerForm-promptSaveTooltip { + position: absolute; + height: 100%; + display: block; + margin-left: 20px; + width: ~"calc(100% - 20px)"; +} + +.SchedulerForm-promptSave { + position: relative; +} diff --git a/awx/ui/client/src/scheduler/schedulerForm.partial.html b/awx/ui/client/src/scheduler/schedulerForm.partial.html index ca6d0558fc..b06b1ea7eb 100644 --- a/awx/ui/client/src/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/scheduler/schedulerForm.partial.html @@ -661,23 +661,30 @@
-
- - - - -
+
+ + + +
+
+ +
+ +
diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html index bb237b77c1..df82cf5d85 100644 --- a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html @@ -8,13 +8,13 @@
+
+ {{ tag.name }} +
-
- {{ tag.name }} -
diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index a94513449e..4e2fe0609a 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -344,7 +344,7 @@ export default ['$compile', 'Attr', 'Icon', } else { // its assumed that options.input_type = checkbox innerTable += ""; } } @@ -368,11 +368,11 @@ export default ['$compile', 'Attr', 'Icon', if (options.mode === 'select') { if (options.input_type === "radio") { //added by JT so that lookup forms can be either radio inputs or check box inputs innerTable += ""; } else { // its assumed that options.input_type = checkbox innerTable += ""; } } else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) { diff --git a/awx/ui/client/src/shared/parse/parse-type-change.factory.js b/awx/ui/client/src/shared/parse/parse-type-change.factory.js index 7c63eb64f3..7728850046 100644 --- a/awx/ui/client/src/shared/parse/parse-type-change.factory.js +++ b/awx/ui/client/src/shared/parse/parse-type-change.factory.js @@ -48,6 +48,7 @@ export default scrollbarStyle: null } }; + scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly); scope[fld + 'codeMirror'].addModes(variableEditModes); scope[fld + 'codeMirror'].showTextArea({ @@ -95,6 +96,7 @@ export default // convert json to yaml try { removeField(fld); + json_obj = JSON.parse(scope[fld]); if ($.isEmptyObject(json_obj)) { scope[fld] = '---'; diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html index d53ca59b76..ebdb08fc98 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html @@ -21,12 +21,7 @@
-
-
- -
+
@@ -52,6 +47,11 @@ {{ tag.name }} | {{ tag.info }}
+
+ +
diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less index 33d1fb8750..8e6b6947d2 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less @@ -34,25 +34,28 @@ .MultiCredential-tagContainer { display: flex; max-width: 100%; + background-color: @default-link; + color: @default-bg; + border-radius: 5px; + padding: 0px 0px 0px 10px; + margin: 3px 10px 3px 0px; +} + +.MultiCredential-tagContainer--disabled { + background-color: @default-icon; } .MultiCredential-tag { - border-radius: 5px; - padding: 2px 10px; - margin: 3px 0px; font-size: 12px; margin-right: 10px; max-width: 100%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - background-color: @default-link; - color: @default-bg; - padding-left: 15px; + padding: 2px 0px 2px 15px; } .MultiCredential-tag--disabled { - background-color: @default-icon; border-top-left-radius: 0px; border-bottom-left-radius: 0px; padding-left: 10px; @@ -60,22 +63,17 @@ .MultiCredential-tag--deletable { margin-right: 0px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; border-right: 0; max-width: ~"calc(100% - 23px)"; - margin-right: 10px; padding-left: 10px; } .MultiCredential-deleteContainer { - background-color: @default-link!important; - color: white; - background-color: @default-bg; - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; - padding: 0 5px; - margin: 3px 0px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + padding: 2px 5px; align-items: center; display: flex; cursor: pointer; @@ -86,8 +84,6 @@ } .MultiCredential-iconContainer { - background-color: @default-link!important; - color: @default-bg; border-top-left-radius: 0px; border-bottom-left-radius: 0px; padding: 0px 5px; @@ -98,8 +94,6 @@ } .MultiCredential-iconContainer--disabled { - background-color: @default-icon; - color: @default-bg; border-top-left-radius: 5px; border-bottom-left-radius: 5px; padding: 0 5px; @@ -112,7 +106,7 @@ .MultiCredential-tagIcon { margin: 0px 0px; - font-size: 10px; + font-size: 12px; } .MultiCredential-name { @@ -123,10 +117,9 @@ .MultiCredential-name--label { color: @default-list-header-bg; - font-size: 10px; + font-size: 12px; margin-left: -8px; margin-right: 5px; - text-transform: uppercase; } .MultiCredential-tag--deletable > .MultiCredential-name { diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html index bdbfa903ce..25692aa177 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html @@ -16,12 +16,8 @@
-
- -
@@ -47,6 +43,11 @@ {{ tag.name }} | {{ tag.info }}
+
+ +
diff --git a/awx/ui/client/src/templates/labels/labelsList.block.less b/awx/ui/client/src/templates/labels/labelsList.block.less index d503a354fa..9c240f630e 100644 --- a/awx/ui/client/src/templates/labels/labelsList.block.less +++ b/awx/ui/client/src/templates/labels/labelsList.block.less @@ -39,27 +39,27 @@ color: @default-link-hov; } -.LabelList-tag--deletable, .JobSubmission-previewTag--deletable { +.LabelList-tag--deletable, .JobSubmission-previewTag--deletable, .Prompt-previewTag--deletable { color: @default-bg; background-color: @default-link; margin-right: 0px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; border-right: 0; max-width: ~"calc(100% - 23px)"; - margin-right: 5px; } -.LabelList-deleteContainer, .JobSubmission-previewTagContainerDelete { +.LabelList-deleteContainer, .JobSubmission-previewTagContainerDelete, .Prompt-previewTagContainerDelete { background-color: @default-link; - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; color: @default-bg; padding: 0 5px; margin: 3px 0px; align-items: center; display: flex; cursor: pointer; + margin-right: 5px; } .LabelList-tagDelete { @@ -76,12 +76,12 @@ max-width: ~"calc(100% - 23px)"; } -.LabelList-deleteContainer:hover, .JobSubmission-previewTagContainerDelete:hover { +.LabelList-deleteContainer:hover, .JobSubmission-previewTagContainerDelete:hover, .Prompt-previewTagContainerDelete:hover { border-color: @default-err; background-color: @default-err; } -.LabelList-deleteContainer:hover > .LabelList-tagDelete, .JobSubmission-previewTagContainerDelete:hover > .JobSubmission-previewTagContainerTagDelete { +.LabelList-deleteContainer:hover > .LabelList-tagDelete, .JobSubmission-previewTagContainerDelete:hover > .JobSubmission-previewTagContainerTagDelete, .Prompt-previewTagContainerDelete:hover > .Prompt-previewTagContainerTagDelete { color: @default-bg; } diff --git a/awx/ui/client/src/templates/labels/labelsList.partial.html b/awx/ui/client/src/templates/labels/labelsList.partial.html index 71e9da8b7e..6275d6c0c6 100644 --- a/awx/ui/client/src/templates/labels/labelsList.partial.html +++ b/awx/ui/client/src/templates/labels/labelsList.partial.html @@ -17,14 +17,14 @@ Labels
+
+ {{ label.name }} +
-
- {{ label.name }} -
View More
diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 53cfe2ce2e..abc320451a 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -10,6 +10,7 @@ import jobTemplates from './job_templates/main'; import workflowAdd from './workflows/add-workflow/main'; import workflowEdit from './workflows/edit-workflow/main'; import labels from './labels/main'; +import prompt from './prompt/main'; import workflowChart from './workflows/workflow-chart/main'; import workflowMaker from './workflows/workflow-maker/main'; import workflowControls from './workflows/workflow-controls/main'; @@ -23,7 +24,7 @@ import TemplatesStrings from './templates.strings'; import listRoute from '~features/templates/list.route.js'; export default -angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, workflowAdd.name, workflowEdit.name, +angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, prompt.name, workflowAdd.name, workflowEdit.name, workflowChart.name, workflowMaker.name, workflowControls.name ]) .service('TemplatesService', templatesService) diff --git a/awx/ui/client/src/templates/prompt/main.js b/awx/ui/client/src/templates/prompt/main.js new file mode 100644 index 0000000000..dd97c78cf1 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/main.js @@ -0,0 +1,17 @@ +import promptDirective from './prompt.directive'; +import promptInventory from './steps/inventory/prompt-inventory.directive'; +import promptCredential from './steps/credential/prompt-credential.directive'; +import promptOtherPrompts from './steps/other-prompts/prompt-other-prompts.directive'; +import promptSurvey from './steps/survey/prompt-survey.directive'; +import promptPreview from './steps/preview/prompt-preview.directive'; +import promptService from './prompt.service'; + +export default + angular.module('prompt', []) + .directive('prompt', promptDirective) + .directive('promptInventory', promptInventory) + .directive('promptCredential', promptCredential) + .directive('promptOtherPrompts', promptOtherPrompts) + .directive('promptSurvey', promptSurvey) + .directive('promptPreview', promptPreview) + .service('PromptService', promptService); diff --git a/awx/ui/client/src/templates/prompt/prompt.block.less b/awx/ui/client/src/templates/prompt/prompt.block.less new file mode 100644 index 0000000000..555d71d09d --- /dev/null +++ b/awx/ui/client/src/templates/prompt/prompt.block.less @@ -0,0 +1,166 @@ +.Prompt .modal-dialog { + width: 700px; +} +.Prompt-step { + margin-top: 20px; +} +.Prompt-footer { + display: flex; + flex: 0 0 auto; + margin-top: 15px; + justify-content: flex-end; + align-items: flex-end; +} +.Prompt-actionButton { + background-color: @submit-button-bg; + border: 1px solid @submit-button-bg; + color: @submit-button-text; + text-transform: uppercase; + border-radius: 5px; + height: 30px; + padding-left:15px; + padding-right: 15px; + min-width: 85px; +} +.Prompt-actionButton:disabled { + background-color: @d7grey; + border-color: @d7grey; + opacity: 0.65; +} +.Prompt-actionButton:enabled:hover, +.Prompt-actionButton:enabled:focus { + background-color: @submit-button-bg-hov; + border: 1px solid @submit-button-bg-hov; +} +.Prompt-defaultButton{ + background-color: @default-bg; + color: @btn-txt; + text-transform: uppercase; + border-radius: 5px; + border: 1px solid @btn-bord; + padding-left:15px; + padding-right: 15px; + height: 30px; + min-width: 85px; + margin-right: 20px; +} +.Prompt-defaultButton:hover{ + background-color: @btn-bg-hov; + color: @btn-txt; +} +.Prompt-revertLink { + font-size: 12px; +} + +.Prompt-selectedItem { + display: flex; + flex: 1 0 auto; + margin-bottom: 15px; + align-items: baseline; +} +.Prompt-selectedItemInfo { + display: flex; + flex: 0 0 100%; + background-color: @default-no-items-bord; + border: 1px solid @default-border; + padding: 10px; + border-radius: 5px; + max-height: 120px; + overflow-y: scroll; +} +.Prompt-selectedItemRevert { + display: flex; + flex: 0 0 auto; +} +.Prompt-credentialSubSection { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 15px; +} +.Prompt-selectedItemLabel, .Prompt-label { + color: @default-interface-txt; + margin-right: 10px; +} +.Prompt-label { + line-height: 24px; +} +.Prompt-selectedItemNone { + color: @default-icon; +} +.Prompt-selectedItemContainer { + display: block; + width: 100%; +} +.Prompt-instructions { + color: @default-interface-txt; + margin-top: 25px; + margin-bottom: 15px; +} +.Prompt-passwordButton { + padding: 4px 13px; +} +.Prompt .List-noItems { + margin-top: auto; +} +.Prompt-selectedItemLabel { + flex: 0 0 80px; + line-height: 29px; +} +.Prompt-previewTags--outer { + flex: 1 0 auto; + max-width: ~"calc(100% - 140px)"; +} +.Prompt-previewTags--inner { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} +.Prompt-previewTagLabel { + color: @default-interface-txt; +} +.Prompt-previewTagLabel--deletable{ + color: @default-list-header-bg; +} +.Prompt-previewTagRevert { + flex: 0 0 60px; + line-height: 29px; +} +.Prompt-previewTagContainer { + display: flex; +} +.Prompt-previewRow--flex { + display: flex; + margin-bottom: 10px; +} +.Prompt-previewRow--noflex { + margin-bottom: 10px; +} +.Prompt-previewRowTitle { + width: 150px; + color: @default-interface-txt; + text-transform: uppercase; +} +.Prompt-previewRowValue { + flex: 1 0 auto; +} +.Prompt-noSelectedItem { + height: 30px; + line-height: 30px; + font-style: italic; + color: @default-interface-txt; +} +.Prompt-previewTag { + border-radius: 5px; + padding: 2px 10px; + margin: 3px 0px; + font-size: 12px; + color: @default-bg; + background-color: @default-link; + margin-right: 5px; + max-width: 100%; + display: inline-block; +} +.Prompt-credentialSubSection .select2 { + width: 50% !important; +} diff --git a/awx/ui/client/src/templates/prompt/prompt.controller.js b/awx/ui/client/src/templates/prompt/prompt.controller.js new file mode 100644 index 0000000000..c2a4563390 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/prompt.controller.js @@ -0,0 +1,188 @@ +export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', 'TemplatesStrings', + function (Rest, GetBasePath, ProcessErrors, CredentialType, strings) { + + // strings.get('deleteResource.HEADER') + // ${strings.get('deleteResource.CONFIRM', 'template')} + + const vm = this || {}; + + vm.strings = strings; + + let scope; + let modal; + + vm.init = (_scope_) => { + scope = _scope_; + ({ modal } = scope[scope.ns]); + + scope.$watch('vm.promptData.triggerModalOpen', () => { + if(vm.promptData && vm.promptData.triggerModalOpen) { + + vm.steps = { + inventory: { + includeStep: false + }, + credential: { + includeStep: false + }, + other_prompts: { + includeStep: false + }, + survey: { + includeStep: false + }, + preview: { + includeStep: true, + tab: { + _active: false, + _disabled: true + } + } + }; + + let order = 1; + + vm.actionText = vm.actionText ? vm.actionText : strings.get('prompt.LAUNCH'); + + vm.forms = {}; + + let credentialType = new CredentialType(); + + credentialType.http.get() + .then( (response) => { + vm.promptData.prompts.credentials.credentialTypes = {}; + vm.promptData.prompts.credentials.credentialTypeOptions = []; + response.data.results.forEach((credentialTypeRow => { + vm.promptData.prompts.credentials.credentialTypes[credentialTypeRow.id] = credentialTypeRow.kind; + if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault)$/)) { + if(credentialTypeRow.kind === 'ssh') { + vm.promptData.prompts.credentials.credentialKind = credentialTypeRow.id.toString(); + } + vm.promptData.prompts.credentials.credentialTypeOptions.push({ + name: credentialTypeRow.name, + value: credentialTypeRow.id + }); + } + })); + + vm.promptData.prompts.inventory.templateDefault = _.has(vm, 'promptData.launchConf.defaults.inventory') ? vm.promptData.launchConf.defaults.inventory : null; + vm.promptData.prompts.credentials.templateDefault = _.has(vm, 'promptData.launchConf.defaults.credentials') ? angular.copy(vm.promptData.launchConf.defaults.credentials) : []; + 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 + }; + + 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 = []; + } + + vm.promptData.prompts.credentials.passwords.vault.push(credPassObj); + } + }); + + } + }); + + vm.promptData.prompts.variables.ignore = vm.promptData.launchConf.ignore_ask_variables; + vm.promptData.prompts.verbosity.templateDefault = vm.promptData.launchConf.defaults.verbosity; + vm.promptData.prompts.jobType.templateDefault = vm.promptData.launchConf.defaults.job_type; + vm.promptData.prompts.limit.templateDefault = vm.promptData.launchConf.defaults.limit; + vm.promptData.prompts.tags.templateDefault = vm.promptData.launchConf.defaults.job_tags; + vm.promptData.prompts.skipTags.templateDefault = vm.promptData.launchConf.defaults.skip_tags; + vm.promptData.prompts.diffMode.templateDefault = vm.promptData.launchConf.defaults.diff_mode; + + if(vm.promptData.launchConf.ask_inventory_on_launch) { + vm.steps.inventory.includeStep = true; + vm.steps.inventory.tab = { + _active: true, + order: order + }; + order++; + } + if(vm.promptData.launchConf.ask_credential_on_launch || (vm.promptData.launchConf.passwords_needed_to_start && vm.promptData.launchConf.passwords_needed_to_start.length > 0)) { + vm.steps.credential.includeStep = true; + vm.steps.credential.tab = { + _active: order === 1 ? true : false, + _disabled: order === 1 ? false : true, + order: order + }; + order++; + } + if(vm.promptData.launchConf.ask_verbosity_on_launch || vm.promptData.launchConf.ask_job_type_on_launch || vm.promptData.launchConf.ask_limit_on_launch || vm.promptData.launchConf.ask_tags_on_launch || vm.promptData.launchConf.ask_skip_tags_on_launch || (vm.promptData.launchConf.ask_variables_on_launch && !vm.promptData.launchConf.ignore_ask_variables) || vm.promptData.launchConf.ask_diff_mode_on_launch) { + vm.steps.other_prompts.includeStep = true; + vm.steps.other_prompts.tab = { + _active: order === 1 ? true : false, + _disabled: order === 1 ? false : true, + order: order + }; + order++; + } + if(vm.promptData.launchConf.survey_enabled) { + vm.steps.survey.includeStep = true; + vm.steps.survey.tab = { + _active: order === 1 ? true : false, + _disabled: order === 1 ? false : true, + order: order + }; + order++; + } + vm.steps.preview.tab.order = order; + modal.show('PROMPT'); + vm.promptData.triggerModalOpen = false; + }) + .catch(({data, status}) => { + ProcessErrors(scope, data, status, null, { + hdr: 'Error!', + msg: 'Failed to get credential types. GET status: ' + status + }); + }); + } + }, true); + }; + + vm.next = (currentTab) => { + Object.keys(vm.steps).forEach(step => { + if(vm.steps[step].tab) { + if(vm.steps[step].tab.order === currentTab.order) { + vm.steps[step].tab._active = false; + } else if(vm.steps[step].tab.order === currentTab.order + 1) { + vm.steps[step].tab._active = true; + vm.steps[step].tab._disabled = false; + } + } + }); + }; + + vm.finish = () => { + vm.promptData.triggerModalOpen = false; + if(vm.onFinish) { + vm.onFinish(); + } + modal.hide(); + }; + + vm.cancel = () => { + vm.promptData.triggerModalOpen = false; + modal.hide(); + }; +}]; diff --git a/awx/ui/client/src/templates/prompt/prompt.directive.js b/awx/ui/client/src/templates/prompt/prompt.directive.js new file mode 100644 index 0000000000..501d89f79a --- /dev/null +++ b/awx/ui/client/src/templates/prompt/prompt.directive.js @@ -0,0 +1,24 @@ +import promptController from './prompt.controller'; +export default [ 'templateUrl', + function(templateUrl) { + return { + scope: { + promptData: '=', + onFinish: '&', + actionText: '@actionText' + }, + templateUrl: templateUrl('templates/prompt/prompt'), + replace: true, + transclude: true, + restrict: 'E', + controller: promptController, + controllerAs: 'vm', + bindToController: true, + link: function(scope, el, attrs, promptController) { + scope.ns = 'launch'; + scope[scope.ns] = { modal: {} }; + + promptController.init(scope); + } + }; +}]; diff --git a/awx/ui/client/src/templates/prompt/prompt.partial.html b/awx/ui/client/src/templates/prompt/prompt.partial.html new file mode 100644 index 0000000000..e2c5c73090 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/prompt.partial.html @@ -0,0 +1,36 @@ +
+ + + {{:: vm.strings.get('prompt.INVENTORY') }} + {{:: vm.strings.get('prompt.CREDENTIAL') }} + {{:: vm.strings.get('prompt.OTHER_PROMPTS') }} + {{:: vm.strings.get('prompt.SURVEY') }} + {{:: vm.strings.get('prompt.PREVIEW') }} + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/awx/ui/client/src/templates/prompt/prompt.service.js b/awx/ui/client/src/templates/prompt/prompt.service.js new file mode 100644 index 0000000000..40840ee888 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/prompt.service.js @@ -0,0 +1,124 @@ +function PromptService (Empty, $filter) { + + this.processPromptValues = (params) => { + let prompts = { + credentials: {}, + inventory: {}, + variables: {}, + verbosity: {}, + jobType: {}, + limit: {}, + tags: {}, + skipTags: {}, + diffMode: {} + }; + + prompts.credentials.value = _.has(params, 'launchConf.defaults.credentials') ? 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 : ""); + + prompts.variables.value = _.has(params, 'launchConf.defaults.extra_vars') ? params.launchConf.defaults.extra_vars : "---"; + 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]})); + prompts.jobType.value = _.has(params, 'currentValues.job_type') && params.currentValues.job_type ? _.find(prompts.jobType.choices, item => item.value === params.currentValues.job_type) : _.find(prompts.jobType.choices, item => item.value === params.launchConf.defaults.job_type); + 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); + + return prompts; + }; + + this.processSurveyQuestions = (params) => { + + let missingSurveyValue = false; + + for(let i=0; i { + if(scope.credentials && scope.credentials.length > 0) { + scope.credentials.forEach((credential, i) => { + scope.credentials[i].checked = 0; + }); + scope.promptData.prompts.credentials.value.forEach((selectedCredential) => { + if(selectedCredential.credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) { + scope.credentials.forEach((credential, i) => { + if(scope.credentials[i].id === selectedCredential.id) { + scope.credentials[i].checked = 1; + } + }); + } + }); + } + }; + + let wipePasswords = (cred) => { + if(cred.passwords_needed) { + cred.passwords_needed.forEach((passwordNeeded => { + if(passwordNeeded === 'ssh_password') { + delete scope.promptData.prompts.credentials.passwords.ssh; + } else if(passwordNeeded === 'become_password') { + delete scope.promptData.prompts.credentials.passwords.become; + } else if(passwordNeeded === 'ssh_key_unlock') { + delete scope.promptData.prompts.credentials.passwords.ssh_key_unlock; + } else if(passwordNeeded.startsWith("vault_password")) { + for (let i = scope.promptData.prompts.credentials.passwords.vault.length - 1; i >= 0; i--) { + if(cred.id === scope.promptData.prompts.credentials.passwords.vault[i].id) { + scope.promptData.prompts.credentials.passwords.vault.splice(i, 1); + } + } + } + })); + } else if(cred.inputs && !_.isEmpty(cred.inputs)) { + if(cred.inputs.password && cred.inputs.password === "ASK") { + delete scope.promptData.prompts.credentials.passwords.ssh; + } else if(cred.inputs.become_password && cred.inputs.become_password === "ASK") { + delete scope.promptData.prompts.credentials.passwords.become; + } else if(cred.inputs.ssh_key_unlock && cred.inputs.ssh_key_unlock === "ASK") { + delete scope.promptData.prompts.credentials.passwords.ssh_key_unlock; + } else if(cred.inputs.vault_password && cred.inputs.vault_password === "ASK") { + for (let i = scope.promptData.prompts.credentials.passwords.vault.length - 1; i >= 0; i--) { + if(cred.id === scope.promptData.prompts.credentials.passwords.vault[i].id) { + scope.promptData.prompts.credentials.passwords.vault.splice(i, 1); + } + } + } + } + + }; + + let updateNeededPasswords = (cred) => { + if(cred.inputs) { + let credPassObj = { + id: cred.id, + name: cred.name + }; + if(cred.inputs.password && cred.inputs.password === "ASK") { + scope.promptData.prompts.credentials.passwords.ssh = credPassObj; + } else if(cred.inputs.become_password && cred.inputs.become_password === "ASK") { + scope.promptData.prompts.credentials.passwords.become = credPassObj; + } else if(cred.inputs.ssh_key_unlock && cred.inputs.ssh_key_unlock === "ASK") { + scope.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj; + } else if(cred.inputs.vault_password && cred.inputs.vault_password === "ASK") { + credPassObj.vault_id = cred.inputs.vault_id; + if(!scope.promptData.prompts.credentials.passwords.vault) { + scope.promptData.prompts.credentials.passwords.vault = []; + } + scope.promptData.prompts.credentials.passwords.vault.push(credPassObj); + } + } + }; + + vm.init = (_scope_, _launch_) => { + scope = _scope_; + launch = _launch_; + + scope.toggle_row = (selectedRow) => { + for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { + if(scope.promptData.prompts.credentials.value[i].credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) { + wipePasswords(scope.promptData.prompts.credentials.value[i]); + scope.promptData.prompts.credentials.value.splice(i, 1); + } + } + + scope.promptData.prompts.credentials.value.push(_.cloneDeep(selectedRow)); + updateNeededPasswords(selectedRow); + }; + + scope.toggle_credential = (cred) => { + // This is a checkbox click. At the time of writing this the only + // multi-select credentials on launch are vault credentials so this + // logic should only get executed when a vault credential checkbox + // is clicked. + + let uncheck = false; + + let removeCredential = (credentialToRemove, index) => { + wipePasswords(credentialToRemove); + scope.promptData.prompts.credentials.value.splice(index, 1); + }; + + // Only one vault credential per vault_id is allowed so we need to check + // to see if one has already been selected and if so replace it. + for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { + if(cred.credential_type === scope.promptData.prompts.credentials.value[i].credential_type) { + if(scope.promptData.prompts.credentials.value[i].id === cred.id) { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + i = -1; + uncheck = true; + } + else if(scope.promptData.prompts.credentials.value[i].inputs) { + if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].inputs.vault_id) { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + } + } else if(scope.promptData.prompts.credentials.value[i].vault_id) { + if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].vault_id) { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + } + } else { + // The currently selected vault credential does not have a vault_id + if(!cred.inputs.vault_id || cred.inputs.vault_id === "") { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + } + } + } + } + + if(!uncheck) { + scope.promptData.prompts.credentials.value.push(cred); + updateNeededPasswords(cred); + } + }; + + scope.credential_dataset = []; + scope.credentials = []; + + let credList = _.cloneDeep(CredentialList); + credList.emptyListText = strings.get('prompt.NO_CREDS_MATCHING_TYPE'); + scope.list = credList; + scope.generateCredentialList(scope.promptData.prompts.credentials.credentialKind); + + scope.credential_default_params = { + order_by: 'name', + page_size: 5 + }; + + scope.credential_queryset = { + order_by: 'name', + page_size: 5 + }; + + scope.$watch('promptData.prompts.credentials.credentialKind', (oldKind, newKind) => { + if (scope.promptData.prompts.credentials && scope.promptData.prompts.credentials.credentialKind) { + if(scope.promptData.prompts.credentials.credentialTypes[oldKind] === "vault" || scope.promptData.prompts.credentials.credentialTypes[newKind] === "vault") { + scope.generateCredentialList(scope.promptData.prompts.credentials.credentialKind); + } + scope.credential_queryset.page = 1; + scope.credential_default_params.credential_type = scope.credential_queryset.credential_type = parseInt(scope.promptData.prompts.credentials.credentialKind); + + qs.search(GetBasePath('credentials'), scope.credential_default_params) + .then(res => { + scope.credential_dataset = res.data; + scope.credentials = scope.credential_dataset.results; + }); + } + }); + + scope.$watchCollection('promptData.prompts.credentials.value', () => { + updateSelectedRow(); + }); + + scope.$watchCollection('credentials', () => { + updateSelectedRow(); + }); + + CreateSelect2({ + element: '#launch-kind-select', + multiple: false + }); + }; + + vm.deleteSelectedCredential = (credentialToDelete) => { + for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { + if(scope.promptData.prompts.credentials.value[i].id === credentialToDelete.id) { + wipePasswords(credentialToDelete); + scope.promptData.prompts.credentials.value.splice(i, 1); + } + } + + scope.credentials.forEach((credential, i) => { + if(credential.id === credentialToDelete.id) { + scope.credentials[i].checked = 0; + } + }); + }; + + vm.revert = () => { + scope.promptData.prompts.credentials.value = scope.promptData.prompts.credentials.templateDefault; + scope.promptData.prompts.credentials.passwords = { + vault: [] + }; + scope.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 + }; + + if(passwordNeeded === "ssh_password") { + scope.promptData.prompts.credentials.passwords.ssh = credPassObj; + } + if(passwordNeeded === "become_password") { + scope.promptData.prompts.credentials.passwords.become = credPassObj; + } + if(passwordNeeded === "ssh_key_unlock") { + scope.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj; + } + if(passwordNeeded.startsWith("vault_password")) { + credPassObj.vault_id = credential.vault_id; + scope.promptData.prompts.credentials.passwords.vault.push(credPassObj); + } + }); + + } + }); + }; + + vm.showRevertCredentials = () => { + if(scope.promptData.launchConf.ask_credential_on_launch) { + if(scope.promptData.prompts.credentials.value && scope.promptData.prompts.credentials.templateDefault && (scope.promptData.prompts.credentials.value.length === scope.promptData.prompts.credentials.templateDefault.length)) { + let selectedIds = scope.promptData.prompts.credentials.value.map((x) => { return x.id; }).sort(); + let defaultIds = scope.promptData.prompts.credentials.templateDefault.map((x) => { return x.id; }).sort(); + return !selectedIds.every((e, i) => { return defaultIds.indexOf(e) === i; }); + } else { + return true; + } + } else { + return false; + } + }; + + vm.togglePassword = (id) => { + var buttonId = id + "_show_input_button", + inputId = id; + if ($(inputId).attr("type") === "password") { + $(buttonId).html(strings.get('HIDE')); + $(inputId).attr("type", "text"); + } else { + $(buttonId).html(strings.get('SHOW')); + $(inputId).attr("type", "password"); + } + }; + } + ]; diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js new file mode 100644 index 0000000000..5d32df89fa --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js @@ -0,0 +1,56 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import promptCredentialController from './prompt-credential.controller'; + +export default [ 'templateUrl', '$compile', 'generateList', + (templateUrl, $compile, GenerateList) => { + return { + scope: { + promptData: '=', + credentialPasswordsForm: '=' + }, + templateUrl: templateUrl('templates/prompt/steps/credential/prompt-credential'), + controller: promptCredentialController, + controllerAs: 'vm', + require: ['^^prompt', 'promptCredential'], + restrict: 'E', + replace: true, + transclude: true, + link: (scope, el, attrs, controllers) => { + + const launchController = controllers[0]; + const promptCredentialController = controllers[1]; + + scope.generateCredentialList = (credKind) => { + let inputType = (credKind && scope.promptData.prompts.credentials.credentialTypes[credKind] === "vault") ? null : 'radio'; + let list = _.cloneDeep(scope.list); + + if(credKind && scope.promptData.prompts.credentials.credentialTypes[credKind] === "vault") { + list.fields.name.modalColumnClass = 'col-md-6'; + list.fields.info = { + label: 'Vault ID', + ngBind: 'credential.inputs.vault_id', + key: false, + nosort: true, + modalColumnClass: 'col-md-6', + infoHeaderClass: '', + dataPlacement: 'top', + }; + } + + let html = GenerateList.build({ + list: list, + input_type: inputType, + mode: 'lookup' + }); + $('#prompt-credential').empty().append($compile(html)(scope)); + }; + + promptCredentialController.init(scope, launchController); + } + }; +}]; diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html new file mode 100644 index 0000000000..9329e4a761 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html @@ -0,0 +1,113 @@ +
+
+
+
+ {{:: vm.strings.get('prompt.SELECTED') }} +
+
+
{{:: vm.strings.get('prompt.NO_CREDENTIALS_SELECTED') }}
+
+
+
+ + + + + + +
+
+ + {{ credential.name }} + + + {{ credential.name }} | {{ credential.vault_id ? credential.vault_id : credential.inputs.vault_id }} + +
+
+ +
+
+
+
+ +
+
+ +
+ {{:: vm.strings.get('prompt.CREDENTIAL_TYPE') }}: + +
+
+
+
+
{{:: vm.strings.get('prompt.PASSWORDS_REQUIRED_HELP') }}
+
+
+ +
+ + + + +
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
+
+
+
+ +
+ + + + +
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
+
+
+
+ +
+ + + + +
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
+
+
+ +
+ +
+ + + + +
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
+
+
+ +
+
diff --git a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js new file mode 100644 index 0000000000..ea22d5fbb7 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js @@ -0,0 +1,37 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ 'TemplatesStrings', function(strings) { + const vm = this; + + vm.strings = strings; + + let scope; + let launch; + + vm.init = (_scope_, _launch_) => { + scope = _scope_; + launch = _launch_; + + scope.toggle_row = (row) => { + scope.promptData.prompts.inventory.value = row; + }; + }; + + vm.deleteSelectedInventory = () => { + scope.promptData.prompts.inventory.value = null; + + scope.inventories.forEach((inventory) => { + inventory.checked = 0; + }); + }; + + vm.revert = () => { + scope.promptData.prompts.inventory.value = scope.promptData.prompts.inventory.templateDefault; + }; + } + ]; diff --git a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js new file mode 100644 index 0000000000..826981c189 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js @@ -0,0 +1,72 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import promptInventoryController from './prompt-inventory.controller'; + +export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList', + (templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) => { + return { + scope: { + promptData: '=' + }, + templateUrl: templateUrl('templates/prompt/steps/inventory/prompt-inventory'), + controller: promptInventoryController, + controllerAs: 'vm', + require: ['^^prompt', 'promptInventory'], + restrict: 'E', + replace: true, + transclude: true, + link: (scope, el, attrs, controllers) => { + + const launchController = controllers[0]; + const promptInventoryController = controllers[1]; + + promptInventoryController.init(scope, launchController); + + scope.inventory_default_params = { + order_by: 'name', + page_size: 5 + }; + + scope.inventory_queryset = { + order_by: 'name', + page_size: 5 + }; + + // Fire off the initial search + qs.search(GetBasePath('inventory'), scope.inventory_default_params) + .then(res => { + scope.inventory_dataset = res.data; + scope.inventories = scope.inventory_dataset.results; + + let invList = _.cloneDeep(InventoryList); + let html = GenerateList.build({ + list: invList, + input_type: 'radio', + mode: 'lookup' + }); + + scope.list = invList; + + $('#prompt-inventory').append($compile(html)(scope)); + + scope.$watch('promptData.prompts.inventory.value', () => { + if(scope.promptData.prompts.inventory.value && scope.promptData.prompts.inventory.value.id) { + // Loop across the inventories and see if one of them should be "checked" + scope.inventories.forEach((row, i) => { + if (row.id === scope.promptData.prompts.inventory.value.id) { + scope.inventories[i].checked = 1; + } + else { + scope.inventories[i].checked = 0; + } + }); + } + }); + }); + } + }; +}]; diff --git a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html new file mode 100644 index 0000000000..f1b3dca8c0 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html @@ -0,0 +1,26 @@ +
+
+
+
+ {{:: vm.strings.get('prompt.SELECTED') }} +
+
+
{{:: vm.strings.get('prompt.NO_INVENTORY_SELECTED') }}
+
+
+
+ {{promptData.prompts.inventory.value.name}} +
+
+ +
+
+
+
+ +
+
+
+
diff --git a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js new file mode 100644 index 0000000000..127c412d88 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js @@ -0,0 +1,92 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + ['ParseTypeChange', 'CreateSelect2', 'TemplatesStrings', function(ParseTypeChange, CreateSelect2, strings) { + const vm = this; + + vm.strings = strings; + + let scope; + let launch; + + vm.init = (_scope_, _launch_) => { + scope = _scope_; + launch = _launch_; + + scope.parseType = 'yaml'; + + // Can't pass otherPrompts.variables.value into ParseTypeChange + // due to the fact that Angular CodeMirror uses scope[string] + // notation. + scope.extraVariables = scope.promptData.prompts.variables.value; + + scope.$watch('extraVariables', () => { + scope.promptData.prompts.variables.value = scope.extraVariables; + }); + + let codemirrorExtraVars = () => { + if(scope.promptData.launchConf.ask_variables_on_launch && !scope.promptData.prompts.variables.ignore) { + ParseTypeChange({ + scope: scope, + variable: 'extraVariables', + field_id: 'job_launch_variables' + }); + } + }; + + if(scope.promptData.launchConf.ask_job_type_on_launch) { + CreateSelect2({ + element: '#job_launch_job_type', + multiple: false + }); + } + + if(scope.promptData.launchConf.ask_verbosity_on_launch) { + CreateSelect2({ + element: '#job_launch_verbosity', + multiple: false + }); + } + + if(scope.promptData.launchConf.ask_tags_on_launch) { + CreateSelect2({ + element: '#job_launch_job_tags', + multiple: true, + addNew: true + }); + } + + if(scope.promptData.launchConf.ask_skip_tags_on_launch) { + CreateSelect2({ + element: '#job_launch_skip_tags', + multiple: true, + addNew: true + }); + } + + if(scope.isActiveStep) { + codemirrorExtraVars(); + } + + scope.$watch('isActiveStep', () => { + if(scope.isActiveStep) { + codemirrorExtraVars(); + } + }); + }; + + vm.toggleDiff = () => { + scope.promptData.prompts.diffMode.value = !scope.promptData.prompts.diffMode.value; + }; + + vm.updateParseType = (parseType) => { + scope.parseType = parseType; + // This function gets added to scope by the ParseTypeChange factory + scope.parseTypeChange('parseType', 'extraVariables'); + }; + } + ]; diff --git a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js new file mode 100644 index 0000000000..c551fc8bd7 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js @@ -0,0 +1,32 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import promptOtherPrompts from './prompt-other-prompts.controller'; + +export default [ 'templateUrl', + (templateUrl) => { + return { + scope: { + promptData: '=', + otherPromptsForm: '=', + isActiveStep: '=' + }, + templateUrl: templateUrl('templates/prompt/steps/other-prompts/prompt-other-prompts'), + controller: promptOtherPrompts, + controllerAs: 'vm', + require: ['^^prompt', 'promptOtherPrompts'], + restrict: 'E', + replace: true, + transclude: true, + link: (scope, el, attrs, controllers) => { + + const launchController = controllers[0]; + const promptOtherPromptsController = controllers[1]; + + promptOtherPromptsController.init(scope, launchController); + } + }; +}]; diff --git a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html new file mode 100644 index 0000000000..dcbf7f5a92 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html @@ -0,0 +1,110 @@ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
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 new file mode 100644 index 0000000000..66e7e00600 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.controller.js @@ -0,0 +1,85 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ 'ParseTypeChange', 'ToJSON', 'TemplatesStrings', function(ParseTypeChange, ToJSON, strings) { + const vm = this; + + vm.strings = strings; + + let scope; + let launch; + + let consolidateTags = (tagModel, tagId) => { + let tags = angular.copy(tagModel); + $(tagId).siblings(".select2").first().find(".select2-selection__choice").each((optionIndex, option) => { + tags.push({ + value: option.title, + name: option.title, + label: option.title + }); + }); + + return [...tags.reduce((map, tag) => map.has(tag.value) ? map : map.set(tag.value, tag), new Map()).values()]; + }; + + vm.init = (_scope_, _launch_) => { + scope = _scope_; + launch = _launch_; + + vm.showJobTags = true; + vm.showSkipTags = true; + + scope.parseType = 'yaml'; + + scope.promptData.extraVars = ToJSON(scope.parseType, scope.promptData.prompts.variables.value, false); + + if(scope.promptData.launchConf.ask_tags_on_launch) { + scope.promptData.prompts.tags.value = consolidateTags(scope.promptData.prompts.tags.value, "#job_launch_job_tags"); + } + + if(scope.promptData.launchConf.ask_skip_tags_on_launch) { + scope.promptData.prompts.skipTags.value = consolidateTags(scope.promptData.prompts.skipTags.value, "#job_launch_skip_tags"); + } + + if(scope.promptData.launchConf.survey_enabled){ + scope.promptData.surveyQuestions.forEach(surveyQuestion => { + // grab all survey questions that have answers + if(surveyQuestion.required || (surveyQuestion.required === false && surveyQuestion.model.toString()!=="")) { + if(!scope.promptData.extraVars) { + scope.promptData.extraVars = {}; + } + scope.promptData.extraVars[surveyQuestion.variable] = surveyQuestion.model; + } + + if(surveyQuestion.required === false && _.isEmpty(surveyQuestion.model)) { + switch (surveyQuestion.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 (surveyQuestion.min === 0) { + scope.promptData.extraVars[surveyQuestion.variable] = ""; + } + break; + } + } + }); + } + + scope.promptExtraVars = $.isEmptyObject(scope.promptData.extraVars) ? '---' : jsyaml.safeDump(scope.promptData.extraVars); + + ParseTypeChange({ + scope: scope, + variable: 'promptExtraVars', + field_id: 'job_launch_preview_variables', + readOnly: true + }); + }; + } + ]; diff --git a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.directive.js b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.directive.js new file mode 100644 index 0000000000..034236cc43 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.directive.js @@ -0,0 +1,30 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import promptPreview from './prompt-preview.controller'; + +export default [ 'templateUrl', + (templateUrl) => { + return { + scope: { + promptData: '=' + }, + templateUrl: templateUrl('templates/prompt/steps/preview/prompt-preview'), + controller: promptPreview, + controllerAs: 'vm', + require: ['^^prompt', 'promptPreview'], + restrict: 'E', + replace: true, + transclude: true, + link: (scope, el, attrs, controllers) => { + + const launchController = controllers[0]; + const promptPreviewController = controllers[1]; + + promptPreviewController.init(scope, launchController); + } + }; +}]; 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 new file mode 100644 index 0000000000..36d1e5099e --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html @@ -0,0 +1,84 @@ +
+
+
{{:: vm.strings.get('prompt.JOB_TYPE') }}
+
+ {{:: vm.strings.get('prompt.PLAYBOOK_RUN') }} + {{:: vm.strings.get('prompt.CHECK') }} +
+
+
+
{{:: vm.strings.get('prompt.CREDENTIAL') }}
+
+
+
+ + + + + + + + + +
+
+
+
+
+
{{:: vm.strings.get('prompt.INVENTORY') }}
+
+
+
+
{{:: vm.strings.get('prompt.LIMIT') }}
+
+
+
+
{{:: vm.strings.get('prompt.VERBOSITY') }}
+
+
+
+
+ {{:: vm.strings.get('prompt.JOB_TAGS') }}  + + + + +
+
+
+
+ {{tag.name}} +
+
+
+
+
+
+ {{:: vm.strings.get('prompt.SKIP_TAGS') }}  + + + + +
+
+
+
+ {{tag.name}} +
+
+
+
+
+
{{:: vm.strings.get('prompt.SHOW_CHANGES') }}
+
+ {{:: vm.strings.get('ON') }} + {{:: vm.strings.get('OFF') }} +
+
+
+
{{:: vm.strings.get('prompt.EXTRA_VARIABLES') }}
+
+ +
+
+
diff --git a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.controller.js b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.controller.js new file mode 100644 index 0000000000..e37afa49fb --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.controller.js @@ -0,0 +1,35 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ 'TemplatesStrings', function(strings) { + const vm = this; + + vm.strings = strings; + + let scope; + let launch; + + vm.init = (_scope_, _launch_) => { + scope = _scope_; + launch = _launch_; + }; + + // This function is used to hide/show the contents of a password + // within a form + vm.togglePassword = (id) => { + var buttonId = id + "_show_input_button", + inputId = id; + if ($(inputId).attr("type") === "password") { + $(buttonId).html(strings.get('HIDE')); + $(inputId).attr("type", "text"); + } else { + $(buttonId).html(strings.get('SHOW')); + $(inputId).attr("type", "password"); + } + }; + } + ]; diff --git a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js new file mode 100644 index 0000000000..80e07fd404 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js @@ -0,0 +1,31 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import promptSurvey from './prompt-survey.controller'; + +export default [ 'templateUrl', + (templateUrl) => { + return { + scope: { + promptData: '=', + surveyForm: '=' + }, + templateUrl: templateUrl('templates/prompt/steps/survey/prompt-survey'), + controller: promptSurvey, + controllerAs: 'vm', + require: ['^^prompt', 'promptSurvey'], + restrict: 'E', + replace: true, + transclude: true, + link: (scope, el, attrs, controllers) => { + + const launchController = controllers[0]; + const promptSurveyController = controllers[1]; + + promptSurveyController.init(scope, launchController); + } + }; +}]; diff --git a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html new file mode 100644 index 0000000000..8ff7aeba52 --- /dev/null +++ b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html @@ -0,0 +1,65 @@ +
+
+ + +
+ +
+
+ +
{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
+
Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.
+
+
+ +
{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
+
Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.
+
+
+
+ + + + + +
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
+
Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.
+
+
+ +
{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
+
{{:: vm.strings.get('prompt.VALID_INTEGER') }}
+
Please enter an answer between {{question.minValue}} and {{question.maxValue}}.
+
+
+ +
{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
+
{{:: vm.strings.get('prompt.VALID_DECIMAL') }}
+
Please enter an answer between {{question.minValue}} and {{question.maxValue}}.
+
+
+
+ + +
+
+
+ + +
+
+