diff --git a/awx/ui/client/src/access/roleList.block.less b/awx/ui/client/src/access/roleList.block.less index 8bc4dd38de..b3e1d635c9 100644 --- a/awx/ui/client/src/access/roleList.block.less +++ b/awx/ui/client/src/access/roleList.block.less @@ -38,6 +38,8 @@ .RoleList-deleteContainer { border: 1px solid @default-second-border; + border-left-color: @default-bg; + background-color: @default-bg; border-top-right-radius: 5px; border-bottom-right-radius: 5px; padding: 0 5px; diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 9799fb76a8..2ea6ea2df8 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -189,6 +189,18 @@ export default dataPlacement: "right", dataContainer: "body" }, + labels: { + label: 'Labels', + type: 'select', + ngOptions: 'label.label for label in labelOptions track by label.value', + multiSelect: true, + addRequired: false, + editRequired: false, + dataTitle: 'Labels', + dataPlacement: 'right', + awPopOver: 'You can add labels to a job template to aid in filtering', + dataContainer: 'body' + }, variables: { label: 'Extra Variables', type: 'textarea', diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js index 3863a6fc84..01b80551c6 100644 --- a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js +++ b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js @@ -11,14 +11,14 @@ 'GetBasePath', 'InventoryList', 'CredentialList', 'ProjectList', 'LookUpInit', 'md5Setup', 'ParseTypeChange', 'Wait', 'Empty', 'ToJSON', 'CallbackHelpInit', 'initSurvey', 'Prompt', 'GetChoices', '$state', - 'CreateSelect2', + 'CreateSelect2', '$q', function( Refresh, $filter, $scope, $rootScope, $compile, $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, ClearScope, GetBasePath, InventoryList, CredentialList, ProjectList, LookUpInit, md5Setup, ParseTypeChange, Wait, Empty, ToJSON, CallbackHelpInit, SurveyControllerInit, Prompt, GetChoices, - $state, CreateSelect2 + $state, CreateSelect2, $q ) { ClearScope(); @@ -113,7 +113,7 @@ } $scope.removeChoicesReady = $scope.$on('choicesReadyVerbosity', function () { selectCount++; - if (selectCount === 2) { + if (selectCount === 3) { var verbosity; // this sets the default options for the selects as specified by the controller. for (verbosity in $scope.verbosity_options) { @@ -145,7 +145,11 @@ element:'#job_templates_job_type', multiple: false }); - + CreateSelect2({ + element:'#job_templates_labels', + multiple: true, + addNew: true + }); CreateSelect2({ element:'#playbook-select', multiple: false @@ -178,6 +182,21 @@ callback: 'choicesReadyVerbosity' }); + Rest.setUrl('api/v1/labels'); + Rest.get() + .success(function (data) { + $scope.labelOptions = data.results + .map((i) => ({label: i.name, value: i.id})); + $scope.$emit("choicesReadyVerbosity"); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to get labels. GET returned ' + + 'status: ' + status + }); + }); + // Update playbook select whenever project value changes selectPlaybook = function (oldValue, newValue) { var url; @@ -265,12 +284,6 @@ } }; - - // $scope.selectPlaybookUnregister = $scope.$watch('project_name', function (newval, oldval) { - // selectPlaybook(oldval, newval); - // checkSCMStatus(oldval, newval); - // }); - // Register a watcher on project_name if ($scope.selectPlaybookUnregister) { $scope.selectPlaybookUnregister(); @@ -311,14 +324,126 @@ } $scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) { Wait('stop'); - if (data.related && data.related.callback) { - Alert('Callback URL', '
Host callbacks are enabled for this template. The callback URL is:
'+ - '' + $scope.callback_server_path + data.related.callback + '
'+ - 'The host configuration key is: ' + $filter('sanitize')(data.host_config_key) + '
', 'alert-info', saveCompleted, null, null, null, true); - } - else { - saveCompleted(); + if (data.related && + data.related.callback) { + Alert('Callback URL', +` +Host callbacks are enabled for this template. The callback URL is:
++ + ${$scope.callback_server_path} + ${data.related.callback} + +
+The host configuration key is: + + ${$filter('sanitize')(data.host_config_key)} + +
+`, + 'alert-info', saveCompleted, null, null, + null, true); } + var orgDefer = $q.defer(); + var associationDefer = $q.defer(); + + Rest.setUrl(data.related.labels); + + var currentLabels = Rest.get() + .then(function(data) { + return data.data.results + .map(val => val.id); + }); + + currentLabels.then(function (current) { + var labelsToAdd = $scope.labels + .map(val => val.value); + var labelsToDisassociate = current + .filter(val => labelsToAdd + .indexOf(val) === -1) + .map(val => ({id: val, disassociate: true})); + var labelsToAssociate = labelsToAdd + .filter(val => current + .indexOf(val) === -1) + .map(val => ({id: val, associate: true})); + var pass = labelsToDisassociate + .concat(labelsToAssociate); + associationDefer.resolve(pass); + }); + + Rest.setUrl(GetBasePath("organizations")); + Rest.get() + .success(function(data) { + orgDefer.resolve(data.results[0].id); + }); + + orgDefer.promise.then(function(orgId) { + var toPost = []; + $scope.newLabels = $scope.newLabels + .map(function(i, val) { + val.organization = orgId; + return val; + }); + + $scope.newLabels.each(function(i, val) { + toPost.push(val); + }); + + associationDefer.promise.then(function(arr) { + toPost = toPost + .concat(arr); + + Rest.setUrl(data.related.labels); + + var defers = []; + for (var i = 0; i < toPost.length; i++) { + defers.push(Rest.post(toPost[i])); + } + $q.all(defers) + .then(function() { + $scope.addedItem = data.id; + + Refresh({ + scope: $scope, + set: 'job_templates', + iterator: 'job_template', + url: $scope.current_url + }); + + if($scope.survey_questions && + $scope.survey_questions.length > 0){ + //once the job template information + // is saved we submit the survey + // info to the correct endpoint + var url = data.url+ 'survey_spec/'; + Rest.setUrl(url); + Rest.post({ name: $scope.survey_name, + description: $scope.survey_description, + spec: $scope.survey_questions }) + .success(function () { + Wait('stop'); + }) + .error(function (data, + status) { + ProcessErrors( + $scope, + data, + status, + form, + { + hdr: 'Error!', + msg: 'Failed to add new ' + + 'survey. Post returned ' + + 'status: ' + + status + }); + }); + } + + saveCompleted(); + }); + }); + }); }); // Save @@ -327,11 +452,13 @@ $scope.invalid_survey = false; // users can't save a survey with a scan job - if($scope.job_type.value === "scan" && $scope.survey_enabled === true){ + if($scope.job_type.value === "scan" && + $scope.survey_enabled === true){ $scope.survey_enabled = false; } // Can't have a survey enabled without a survey - if($scope.survey_enabled === true && $scope.survey_exists!==true){ + if($scope.survey_enabled === true && + $scope.survey_exists!==true){ $scope.survey_enabled = false; } @@ -341,70 +468,61 @@ try { for (fld in form.fields) { - if (form.fields[fld].type === 'select' && fld !== 'playbook') { + if (form.fields[fld].type === 'select' && + fld !== 'playbook') { data[fld] = $scope[fld].value; } else { - if (fld !== 'variables' && fld !== 'survey') { + if (fld !== 'variables' && + fld !== 'survey') { data[fld] = $scope[fld]; } } } - data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); - if(data.job_type === 'scan' && $scope.default_scan === true){ + data.extra_vars = ToJSON($scope.parseType, + $scope.variables, true); + if(data.job_type === 'scan' && + $scope.default_scan === true){ data.project = ""; data.playbook = ""; } - // We only want to set the survey_enabled flag to true for this job template if a survey exists - // and it's been enabled. By default, survey_enabled is explicitly set to true but if no survey - // is created then we don't want it enabled. - data.survey_enabled = ($scope.survey_enabled && $scope.survey_exists) ? $scope.survey_enabled : false; + // We only want to set the survey_enabled flag to + // true for this job template if a survey exists + // and it's been enabled. By default, + // survey_enabled is explicitly set to true but + // if no survey is created then we don't want + // it enabled. + data.survey_enabled = ($scope.survey_enabled && + $scope.survey_exists) ? $scope.survey_enabled : false; + + $scope.newLabels = $("#job_templates_labels > option") + .filter("[data-select2-tag=true]") + .map((i, val) => ({name: $(val).text()})); + Rest.setUrl(defaultUrl); Rest.post(data) .success(function(data) { - $scope.$emit('templateSaveSuccess', data); - - $scope.addedItem = data.id; - - Refresh({ - scope: $scope, - set: 'job_templates', - iterator: 'job_template', - url: $scope.current_url - }); - - if($scope.survey_questions && $scope.survey_questions.length > 0){ - //once the job template information is saved we submit the survey info to the correct endpoint - var url = data.url+ 'survey_spec/'; - Rest.setUrl(url); - Rest.post({ name: $scope.survey_name, description: $scope.survey_description, spec: $scope.survey_questions }) - .success(function () { - Wait('stop'); - - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new survey. Post returned status: ' + status }); - }); - } - - + $scope.$emit('templateSaveSuccess', + data); }) .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new job template. POST returned status: ' + status - }); + ProcessErrors($scope, data, status, form, + { + hdr: 'Error!', + msg: 'Failed to add new job ' + + 'template. POST returned status: ' + + status + }); }); } catch (err) { Wait('stop'); - Alert("Error", "Error parsing extra variables. Parser returned: " + err); + Alert("Error", "Error parsing extra variables. " + + "Parser returned: " + err); } - }; $scope.formCancel = function () { $state.transitionTo('jobTemplates'); }; } - ]; diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index c8a2bdcd95..04cdfb80e5 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -21,7 +21,7 @@ export default 'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate', 'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit', 'PlaybookRun' , 'initSurvey', '$state', 'CreateSelect2', - 'ToggleNotification', 'NotificationsListInit', + 'ToggleNotification', 'NotificationsListInit', '$q', function( $filter, $scope, $rootScope, $compile, $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, @@ -31,7 +31,7 @@ export default Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit, JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit, SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit, $state, - CreateSelect2, ToggleNotification, NotificationsListInit + CreateSelect2, ToggleNotification, NotificationsListInit, $q ) { ClearScope(); @@ -232,11 +232,6 @@ export default multiple: false }); - CreateSelect2({ - element:'#job_templates_verbosity', - multiple: false - }); - for (var set in relatedSets) { $scope.search(relatedSets[set].iterator); } @@ -353,7 +348,7 @@ export default } $scope.removeChoicesReady = $scope.$on('choicesReady', function() { choicesCount++; - if (choicesCount === 4) { + if (choicesCount === 5) { $scope.$emit('LoadJobs'); } }); @@ -392,6 +387,41 @@ export default callback: 'choicesReady' }); + Rest.setUrl('api/v1/labels'); + Wait("start"); + Rest.get() + .success(function (data) { + $scope.labelOptions = data.results + .map((i) => ({label: i.name, value: i.id})); + $scope.$emit("choicesReady"); + Rest.setUrl(defaultUrl + $state.params.template_id + + "/labels"); + Rest.get() + .success(function(data) { + var opts = data.results + .map(i => ({id: i.id + "", + test: i.name})); + CreateSelect2({ + element:'#job_templates_labels', + multiple: true, + addNew: true, + opts: opts + }); + Wait("stop"); + }); + CreateSelect2({ + element:'#job_templates_verbosity', + multiple: false + }); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to get labels. GET returned ' + + 'status: ' + status + }); + }); + function saveCompleted() { $state.go('jobTemplates', null, {reload: true}); } @@ -401,34 +431,105 @@ export default } $scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) { Wait('stop'); - if ($scope.allow_callbacks && ($scope.host_config_key !== master.host_config_key || $scope.callback_url !== master.callback_url)) { - if (data.related && data.related.callback) { - Alert('Callback URL', 'Host callbacks are enabled for this template. The callback URL is:
'+ - '' + $scope.callback_server_path + data.related.callback + '
'+ - 'The host configuration key is: ' + $filter('sanitize')(data.host_config_key) + '
', 'alert-info', saveCompleted, null, null, null, true); - } - else { - saveCompleted(); - } - } - else { - saveCompleted(); + if (data.related && + data.related.callback) { + Alert('Callback URL', +` +Host callbacks are enabled for this template. The callback URL is:
++ + ${$scope.callback_server_path} + ${data.related.callback} + +
+The host configuration key is: + + ${$filter('sanitize')(data.host_config_key)} + +
+`, + 'alert-info', saveCompleted, null, null, + null, true); } + var orgDefer = $q.defer(); + var associationDefer = $q.defer(); + + Rest.setUrl(data.related.labels); + + var currentLabels = Rest.get() + .then(function(data) { + return data.data.results + .map(val => val.id); + }); + + currentLabels.then(function (current) { + var labelsToAdd = $scope.labels + .map(val => val.value); + var labelsToDisassociate = current + .filter(val => labelsToAdd + .indexOf(val) === -1) + .map(val => ({id: val, disassociate: true})); + var labelsToAssociate = labelsToAdd + .filter(val => current + .indexOf(val) === -1) + .map(val => ({id: val, associate: true})); + var pass = labelsToDisassociate + .concat(labelsToAssociate); + associationDefer.resolve(pass); + }); + + Rest.setUrl(GetBasePath("organizations")); + Rest.get() + .success(function(data) { + orgDefer.resolve(data.results[0].id); + }); + + orgDefer.promise.then(function(orgId) { + var toPost = []; + $scope.newLabels = $scope.newLabels + .map(function(i, val) { + val.organization = orgId; + return val; + }); + + $scope.newLabels.each(function(i, val) { + toPost.push(val); + }); + + associationDefer.promise.then(function(arr) { + toPost = toPost + .concat(arr); + + Rest.setUrl(data.related.labels); + + var defers = []; + for (var i = 0; i < toPost.length; i++) { + defers.push(Rest.post(toPost[i])); + } + $q.all(defers) + .then(function() { + saveCompleted(); + }); + }); + }); }); // Save changes to the parent + // Save $scope.formSave = function () { var fld, data = {}; $scope.invalid_survey = false; // users can't save a survey with a scan job - if($scope.job_type.value === "scan" && $scope.survey_enabled === true){ + if($scope.job_type.value === "scan" && + $scope.survey_enabled === true){ $scope.survey_enabled = false; } // Can't have a survey enabled without a survey - if($scope.survey_enabled === true && $scope.survey_exists!==true){ + if($scope.survey_enabled === true && + $scope.survey_exists!==true){ $scope.survey_enabled = false; } @@ -437,21 +538,38 @@ export default Wait('start'); try { - // Make sure we have valid variable data - data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); - if(data.extra_vars === undefined ){ - throw 'undefined variables'; - } for (fld in form.fields) { - if (form.fields[fld].type === 'select' && fld !== 'playbook') { + if (form.fields[fld].type === 'select' && + fld !== 'playbook') { data[fld] = $scope[fld].value; } else { - if (fld !== 'variables' && fld !== 'callback_url') { + if (fld !== 'variables' && + fld !== 'survey') { data[fld] = $scope[fld]; } } } - Rest.setUrl(defaultUrl + id + '/'); + data.extra_vars = ToJSON($scope.parseType, + $scope.variables, true); + if(data.job_type === 'scan' && + $scope.default_scan === true){ + data.project = ""; + data.playbook = ""; + } + // We only want to set the survey_enabled flag to + // true for this job template if a survey exists + // and it's been enabled. By default, + // survey_enabled is explicitly set to true but + // if no survey is created then we don't want + // it enabled. + data.survey_enabled = ($scope.survey_enabled && + $scope.survey_exists) ? $scope.survey_enabled : false; + + $scope.newLabels = $("#job_templates_labels > option") + .filter("[data-select2-tag=true]") + .map((i, val) => ({name: $(val).text()})); + + Rest.setUrl(defaultUrl + $state.params.template_id); Rest.put(data) .success(function (data) { $scope.$emit('templateSaveSuccess', data); @@ -460,12 +578,11 @@ export default ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update job template. PUT returned status: ' + status }); }); - } catch (err) { Wait('stop'); - Alert("Error", "Error parsing extra variables. Parser returned: " + err); + Alert("Error", "Error parsing extra variables. " + + "Parser returned: " + err); } - }; $scope.formCancel = function () { @@ -474,7 +591,7 @@ export default var defaultUrl = GetBasePath('job_templates') + $state.params.template_id; Rest.setUrl(defaultUrl); Rest.destroy() - .success(function(res){ + .success(function(){ $state.go('jobTemplates', null, {reload: true, notify:true}); }) .error(function(res, status){ diff --git a/awx/ui/client/src/job-templates/labels/labelsList.block.less b/awx/ui/client/src/job-templates/labels/labelsList.block.less new file mode 100644 index 0000000000..a736558b18 --- /dev/null +++ b/awx/ui/client/src/job-templates/labels/labelsList.block.less @@ -0,0 +1,74 @@ +/** @define LabelList */ +@import "../shared/branding/colors.default.less"; + +.LabelList { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} + +.LabelList-tagContainer { + display: flex; + max-width: 100%; +} + +.LabelList-tag { + border-radius: 5px; + padding: 2px 10px; + margin: 4px 0px; + border: 1px solid @default-second-border; + font-size: 12px; + color: @default-interface-txt; + text-transform: uppercase; + background-color: @default-bg; + margin-right: 5px; + max-width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.LabelList-tag--deletable { + margin-right: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-right: 0; + max-wdith: ~"calc(100% - 23px)"; +} + +.LabelList-deleteContainer { + border: 1px solid @default-second-border; + border-left-color: @default-bg; + background-color: @default-bg; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + padding: 0 5px; + margin: 4px 0px; + margin-right: 5px; + align-items: center; + display: flex; + cursor: pointer; +} + +.LabelList-tagDelete { + font-size: 13px; + color: @default-icon; +} + +.LabelList-name { + flex: initial; + max-width: 100%; +} + +.LabelList-tag--deletable > .LabelList-name { + max-width: ~"calc(100% - 23px)"; +} + +.LabelList-deleteContainer:hover, { + border-color: @default-err; + background-color: @default-err; +} + +.LabelList-deleteContainer:hover > .LabelList-tagDelete { + color: @default-bg; +} diff --git a/awx/ui/client/src/job-templates/labels/labelsList.directive.js b/awx/ui/client/src/job-templates/labels/labelsList.directive.js new file mode 100644 index 0000000000..6df8b36025 --- /dev/null +++ b/awx/ui/client/src/job-templates/labels/labelsList.directive.js @@ -0,0 +1,45 @@ +/* jshint unused: vars */ +export default + [ 'templateUrl', + 'Wait', + 'Rest', + 'GetBasePath', + 'ProcessErrors', + 'Prompt', + function(templateUrl, Wait, Rest, GetBasePath, ProcessErrors, Prompt) { + return { + restrict: 'E', + scope: false, + templateUrl: templateUrl('job-templates/labels/labelsList'), + link: function(scope, element, attrs) { + scope.labels = scope. + job_template.summary_fields.labels; + + scope.deleteLabel = function(templateId, templateName, labelId, labelName) { + var action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = GetBasePath("job_templates") + templateId + "/labels/"; + Rest.setUrl(url); + Rest.post({"disassociate": true, "id": labelId}) + .success(function () { + Wait('stop'); + scope.search("job_template"); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Could not disacssociate label from JT. Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + Prompt({ + hdr: 'Remove Label from ' + templateName, + body: '