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.");