diff --git a/awx/ui/client/legacy/styles/forms.less b/awx/ui/client/legacy/styles/forms.less index 22cd14ac27..bff2d4aff6 100644 --- a/awx/ui/client/legacy/styles/forms.less +++ b/awx/ui/client/legacy/styles/forms.less @@ -169,11 +169,14 @@ border-color: @default-icon; } -.Form-tab--disabled { +.Form-tab--disabled, +.Form-button--disabled { opacity: 0.65; + color: @btn-txt; } -.Form-tab--disabled:hover { +.Form-tab--disabled:hover, +.Form-button--disabled:hover { color: @btn-txt; background-color: @btn-bg; cursor:not-allowed!important; @@ -548,7 +551,6 @@ input[type='radio']:checked:before { } } -.FormToggle {} .FormToggle-container { margin: 0 0 0 10px; display: initial; @@ -607,7 +609,7 @@ input[type='radio']:checked:before { display: flex; justify-content: flex-end; - button:last-of-type { + button { margin-left: 20px; } } @@ -658,7 +660,6 @@ input[type='radio']:checked:before { transition: background-color 0.2s; padding-left:15px; padding-right: 15px; - margin-left: 20px; } .Form-cancelButton:hover { @@ -677,12 +678,17 @@ input[type='radio']:checked:before { margin-bottom: 20px; } +.Form-buttons .Form-primaryButton { + margin-right: 0; +} + .Form-primaryButton:hover { background-color: @default-link-hov; color: @default-bg; } -.Form-primaryButton.Form-tab--disabled:hover { +.Form-primaryButton.Form-tab--disabled:hover, +.Form-primaryButton.Form-button--disabled:hover { background-color: @default-link; } diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index 48de0e5ab5..78edb30b85 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -105,7 +105,9 @@ function ComponentsStrings (BaseString) { }; ns.launchTemplate = { - DEFAULT: t.s('Start a job using this template') + DEFAULT: t.s('Start a job using this template'), + DISABLED: t.s('Please save before launching this template.'), + BUTTON_LABEL: t.s('LAUNCH') }; ns.list = { diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js index 4574ef6fc5..b3dbb4bafc 100644 --- a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js +++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js @@ -3,7 +3,9 @@ import templateUrl from './launchTemplateButton.partial.html'; const atLaunchTemplate = { templateUrl, bindings: { - template: '<' + template: '<', + showTextButton: '<', + disabled: '=' }, controller: ['JobTemplateModel', 'WorkflowJobTemplateModel', 'PromptService', '$state', 'ComponentsStrings', 'ProcessErrors', '$scope', 'TemplatesStrings', 'Alert', @@ -19,6 +21,11 @@ function atLaunchTemplateCtrl ( const jobTemplate = new JobTemplate(); const workflowTemplate = new WorkflowTemplate(); vm.strings = componentsStrings; + vm.strings.get('launchTemplate.DEFAULT'); + + $scope.$watch('vm.disabled', (val) => { + vm.launchTooltip = (val) ? vm.strings.get('launchTemplate.DISABLED') : vm.strings.get('launchTemplate.DEFAULT'); + }); const createErrorHandler = (path, action) => ({ data, status }) => { @@ -28,101 +35,103 @@ function atLaunchTemplateCtrl ( }; vm.startLaunchTemplate = () => { - if (vm.template.type === 'job_template') { - const selectedJobTemplate = jobTemplate.create(); - const preLaunchPromises = [ - selectedJobTemplate.getLaunch(vm.template.id), - selectedJobTemplate.optionsLaunch(vm.template.id), - ]; + if (!vm.disabled) { + if (vm.template.type === 'job_template') { + const selectedJobTemplate = jobTemplate.create(); + const preLaunchPromises = [ + selectedJobTemplate.getLaunch(vm.template.id), + selectedJobTemplate.optionsLaunch(vm.template.id), + ]; - Promise.all(preLaunchPromises) - .then(([launchData, launchOptions]) => { - if (selectedJobTemplate.canLaunchWithoutPrompt()) { - selectedJobTemplate - .postLaunch({ id: vm.template.id }) - .then(({ data }) => { - /* Slice Jobs: Redirect to WF Details page if returned - job type is a WF job */ - if (data.type === 'workflow_job' && data.workflow_job !== null) { - $state.go('workflowResults', { id: data.workflow_job }, { reload: true }); - } else { - $state.go('output', { id: data.job, type: 'playbook' }, { reload: true }); - } - }); - } else { - const promptData = { - launchConf: launchData.data, - launchOptions: launchOptions.data, - template: vm.template.id, - templateType: vm.template.type, - prompts: PromptService.processPromptValues({ + Promise.all(preLaunchPromises) + .then(([launchData, launchOptions]) => { + if (selectedJobTemplate.canLaunchWithoutPrompt()) { + selectedJobTemplate + .postLaunch({ id: vm.template.id }) + .then(({ data }) => { + /* Slice Jobs: Redirect to WF Details page if returned + job type is a WF job */ + if (data.type === 'workflow_job' && data.workflow_job !== null) { + $state.go('workflowResults', { id: data.workflow_job }, { reload: true }); + } else { + $state.go('output', { id: data.job, type: 'playbook' }, { reload: true }); + } + }); + } else { + const promptData = { launchConf: launchData.data, - launchOptions: launchOptions.data - }), - triggerModalOpen: true - }; + launchOptions: launchOptions.data, + template: vm.template.id, + templateType: vm.template.type, + prompts: PromptService.processPromptValues({ + launchConf: launchData.data, + launchOptions: launchOptions.data + }), + triggerModalOpen: true + }; - if (launchData.data.survey_enabled) { - selectedJobTemplate.getSurveyQuestions(vm.template.id) - .then(({ data }) => { - const processed = PromptService.processSurveyQuestions({ - surveyQuestions: data.spec + if (launchData.data.survey_enabled) { + selectedJobTemplate.getSurveyQuestions(vm.template.id) + .then(({ data }) => { + const processed = PromptService.processSurveyQuestions({ + surveyQuestions: data.spec + }); + promptData.surveyQuestions = processed.surveyQuestions; + vm.promptData = promptData; }); - promptData.surveyQuestions = processed.surveyQuestions; - vm.promptData = promptData; + } else { + vm.promptData = promptData; + } + } + }); + } else if (vm.template.type === 'workflow_job_template') { + const selectedWorkflowJobTemplate = workflowTemplate.create(); + const preLaunchPromises = [ + selectedWorkflowJobTemplate.request('get', vm.template.id), + selectedWorkflowJobTemplate.getLaunch(vm.template.id), + selectedWorkflowJobTemplate.optionsLaunch(vm.template.id), + ]; + + Promise.all(preLaunchPromises) + .then(([wfjtData, launchData, launchOptions]) => { + if (selectedWorkflowJobTemplate.canLaunchWithoutPrompt()) { + selectedWorkflowJobTemplate + .postLaunch({ id: vm.template.id }) + .then(({ data }) => { + $state.go('workflowResults', { id: data.workflow_job }, { reload: true }); }); } else { - vm.promptData = promptData; - } - } - }); - } else if (vm.template.type === 'workflow_job_template') { - const selectedWorkflowJobTemplate = workflowTemplate.create(); - const preLaunchPromises = [ - selectedWorkflowJobTemplate.request('get', vm.template.id), - selectedWorkflowJobTemplate.getLaunch(vm.template.id), - selectedWorkflowJobTemplate.optionsLaunch(vm.template.id), - ]; + launchData.data.defaults.extra_vars = wfjtData.data.extra_vars; - Promise.all(preLaunchPromises) - .then(([wfjtData, launchData, launchOptions]) => { - if (selectedWorkflowJobTemplate.canLaunchWithoutPrompt()) { - selectedWorkflowJobTemplate - .postLaunch({ id: vm.template.id }) - .then(({ data }) => { - $state.go('workflowResults', { id: data.workflow_job }, { reload: true }); - }); - } else { - launchData.data.defaults.extra_vars = wfjtData.data.extra_vars; - - const promptData = { - launchConf: selectedWorkflowJobTemplate.getLaunchConf(), - launchOptions: launchOptions.data, - template: vm.template.id, - templateType: vm.template.type, - prompts: PromptService.processPromptValues({ + const promptData = { launchConf: selectedWorkflowJobTemplate.getLaunchConf(), - launchOptions: launchOptions.data - }), - triggerModalOpen: true, - }; + launchOptions: launchOptions.data, + template: vm.template.id, + templateType: vm.template.type, + prompts: PromptService.processPromptValues({ + launchConf: selectedWorkflowJobTemplate.getLaunchConf(), + launchOptions: launchOptions.data + }), + triggerModalOpen: true, + }; - if (launchData.data.survey_enabled) { - selectedWorkflowJobTemplate.getSurveyQuestions(vm.template.id) - .then(({ data }) => { - const processed = PromptService.processSurveyQuestions({ - surveyQuestions: data.spec + if (launchData.data.survey_enabled) { + selectedWorkflowJobTemplate.getSurveyQuestions(vm.template.id) + .then(({ data }) => { + const processed = PromptService.processSurveyQuestions({ + surveyQuestions: data.spec + }); + promptData.surveyQuestions = processed.surveyQuestions; + vm.promptData = promptData; }); - promptData.surveyQuestions = processed.surveyQuestions; - vm.promptData = promptData; - }); - } else { - vm.promptData = promptData; + } else { + vm.promptData = promptData; + } } - } - }); - } else { - Alert(templatesStrings.get('error.UNKNOWN'), templatesStrings.get('alert.UNKNOWN_LAUNCH')); + }); + } else { + Alert(templatesStrings.get('error.UNKNOWN'), templatesStrings.get('alert.UNKNOWN_LAUNCH')); + } } }; diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html index fc5c90de57..41fbbecca7 100644 --- a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html +++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.partial.html @@ -1,9 +1,12 @@
-
diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 4c31bd459f..b28b42aabe 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -591,6 +591,7 @@ angular.module('Utilities', ['RestServices', 'Utilities']) addNew = params.addNew, scope = params.scope, selectOptions = params.options, + callback = params.callback, model = params.model, original_options, minimumResultsForSearch = params.minimumResultsForSearch ? params.minimumResultsForSearch : Infinity; @@ -704,6 +705,10 @@ angular.module('Utilities', ['RestServices', 'Utilities']) $(element).trigger('change'); } + if (callback) { + scope.$emit(callback); + } + }); }; } diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 401a1f11f9..0e484f5f00 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1656,90 +1656,94 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (typeof this.form.buttons[btn] === 'object') { button = this.form.buttons[btn]; - // Set default color and label for Save and Reset - if (btn === 'save') { - button.label = i18n._('Save'); - button['class'] = 'Form-saveButton'; - } - if (btn === 'select') { - button.label = i18n._('Select'); - button['class'] = 'Form-saveButton'; - } - if (btn === 'cancel') { - button.label = i18n._('Cancel'); - button['class'] = 'Form-cancelButton'; - } - if (btn === 'close') { - button.label = i18n._('Close'); - button['class'] = 'Form-cancelButton'; - } - if (btn === 'launch') { - button.label = i18n._('Launch'); - button['class'] = 'Form-launchButton'; - } - if (btn === 'add_survey') { - button.label = i18n._('Add Survey'); - button['class'] = 'Form-surveyButton'; - } - if (btn === 'edit_survey') { - button.label = i18n._('Edit Survey'); - button['class'] = 'Form-surveyButton'; - } - if (btn === 'view_survey') { - button.label = i18n._('View Survey'); - button['class'] = 'Form-surveyButton'; - } - if (btn === 'workflow_visualizer') { - button.label = i18n._('Workflow Visualizer'); - button['class'] = 'Form-primaryButton'; - } - - // Build button HTML - html += "\n"; } } html += "\n"; diff --git a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js index 097b905699..b9b2c15ac2 100644 --- a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js +++ b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js @@ -31,6 +31,7 @@ const jobTemplate = resolvedModels[0]; $scope.canAddJobTemplate = jobTemplate.options('actions.POST'); + $scope.disableLaunch = true; // apply form definition's default field values GenerateForm.applyDefaults(form, $scope); diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index b63dad2feb..49d3e6af15 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -45,6 +45,7 @@ export default id = $stateParams.job_template_id, callback, choicesCount = 0, + select2Count = 0, instance_group_url = defaultUrl + id + '/instance_groups'; init(); @@ -184,33 +185,71 @@ export default function jobTemplateLoadFinished(){ CreateSelect2({ element:'#job_template_job_type', - multiple: false + multiple: false, + callback: 'select2Loaded', + scope: $scope }); CreateSelect2({ element:'#playbook-select', - multiple: false + multiple: false, + callback: 'select2Loaded', + scope: $scope }); CreateSelect2({ element:'#job_template_job_tags', multiple: true, - addNew: true + addNew: true, + callback: 'select2Loaded', + scope: $scope }); CreateSelect2({ element:'#job_template_skip_tags', multiple: true, - addNew: true + addNew: true, + callback: 'select2Loaded', + scope: $scope }); CreateSelect2({ element: '#job_template_custom_virtualenv', multiple: false, - opts: $scope.custom_virtualenvs_options + opts: $scope.custom_virtualenvs_options, + callback: 'select2Loaded', + scope: $scope }); } + $scope.$on('select2Loaded', () => { + select2Count++; + if (select2Count === 10) { + $scope.$emit('select2LoadFinished'); + } + }); + + $scope.$on('select2LoadFinished', () => { + // updates based on lookups will initially set the form as dirty. + // we need to set it as pristine when it contains the values given by the api + // so that we can enable launching when the two are the same + $scope.job_template_form.$setPristine(); + // this is used to set the overall form as dirty for the values + // that don't actually set this internally (lookups, toggles and code mirrors). + $scope.$watchGroup([ + 'inventory', + 'project', + 'multiCredential.selectedCredentials', + 'extra_vars', + 'diff_mode', + 'instance_groups' + ], (val, prevVal) => { + if (!_.isEqual(val, prevVal)) { + $scope.job_template_form.$setDirty(); + } + }); + }); + $scope.toggleForm = function(key) { $scope[key] = !$scope[key]; }; diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index 3c966e1517..5bb952533c 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -405,6 +405,13 @@ function(NotificationsList, i18n) { ngClick: 'formSave()', //$scope.function to call on click, optional ngDisabled: "job_template_form.$invalid",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' + }, + launch: { + component: 'at-launch-template', + templateObj: 'job_template_obj', + ngShow: '(job_template_obj.summary_fields.user_capabilities.start || canAddJobTemplate)', + ngDisabled: 'disableLaunch || job_template_form.$dirty', + showTextButton: 'true' } }, diff --git a/awx/ui/client/src/templates/workflows.form.js b/awx/ui/client/src/templates/workflows.form.js index c0a2418614..94f972bc11 100644 --- a/awx/ui/client/src/templates/workflows.form.js +++ b/awx/ui/client/src/templates/workflows.form.js @@ -147,6 +147,13 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { ngClick: 'formSave()', //$scope.function to call on click, optional ngDisabled: "workflow_job_template_form.$invalid || can_edit!==true", //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)' + }, + launch: { + component: 'at-launch-template', + templateObj: 'workflow_job_template_obj', + ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.start || canAddWorkflowJobTemplate)', + ngDisabled: 'disableLaunch || workflow_job_template_form.$dirty', + showTextButton: 'true' } }, diff --git a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js index 72d02d89e7..5e2e765723 100644 --- a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js +++ b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js @@ -26,6 +26,7 @@ export default [ $scope.canEditInventory = true; $scope.parseType = 'yaml'; $scope.can_edit = true; + $scope.disableLaunch = true; // apply form definition's default field values GenerateForm.applyDefaults(form, $scope); diff --git a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js index 174ffffeef..a8c857f87e 100644 --- a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js +++ b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js @@ -17,6 +17,7 @@ export default [ TemplatesService, Rest, ToggleNotification, OrgAdminLookup, availableLabels, selectedLabels, workflowJobTemplateData, i18n, workflowLaunch, $transitions, WorkflowJobTemplate, Inventory, isNotificationAdmin ) { + let select2Count = 0; $scope.missingTemplates = _.has(workflowLaunch, 'node_templates_missing') && workflowLaunch.node_templates_missing.length > 0 ? true : false; @@ -240,13 +241,6 @@ export default [ }); }; - // Select2-ify the lables input - CreateSelect2({ - element:'#workflow_job_template_labels', - multiple: true, - addNew: true - }); - SurveyControllerInit({ scope: $scope, parent_scope: $scope, @@ -261,11 +255,39 @@ export default [ .map(i => ({id: i.id + "", test: i.name})); + // Select2-ify the lables input CreateSelect2({ + scope: $scope, element:'#workflow_job_template_labels', multiple: true, addNew: true, - opts: opts + opts, + callback: 'select2Loaded' + }); + + $scope.$on('select2Loaded', () => { + select2Count++; + if (select2Count === 1) { + $scope.$emit('select2LoadFinished'); + } + }); + + $scope.$on('select2LoadFinished', () => { + // updates based on lookups will initially set the form as dirty. + // we need to set it as pristine when it contains the values given by the api + // so that we can enable launching when the two are the same + $scope.workflow_job_template_form.$setPristine(); + // this is used to set the overall form as dirty for the values + // that don't actually set this internally (lookups, toggles and code mirrors). + $scope.$watchGroup([ + 'organization', + 'inventory', + 'variables' + ], (val, prevVal) => { + if (!_.isEqual(val, prevVal)) { + $scope.workflow_job_template_form.$setDirty(); + } + }); }); $scope.workflowVisualizerTooltip = i18n._("Click here to open the workflow visualizer.");