From 7a927825ceaa9ef0da05c6d09628743ff40346e7 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 25 May 2017 11:47:40 -0400 Subject: [PATCH 1/6] add scm inv source ui --- awx/ui/client/legacy-styles/ansible-ui.less | 4 + .../sources/add/sources-add.controller.js | 248 ++++++++++++++---- .../sources/edit/sources-edit.controller.js | 199 ++++++++++++-- .../get-source-type-options.factory.js | 2 +- .../sources/list/sources-list.controller.js | 8 +- .../src/inventories/sources/sources.form.js | 76 +++++- .../src/inventories/sources/sources.list.js | 4 +- awx/ui/client/src/projects/projects.list.js | 1 + awx/ui/client/src/shared/Utilities.js | 22 +- awx/ui/client/src/shared/directives.js | 10 +- awx/ui/client/src/shared/generator-helpers.js | 1 + 11 files changed, 482 insertions(+), 93 deletions(-) diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 4a7471c060..7310f1072f 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -2348,3 +2348,7 @@ input[disabled].ui-spinner-input { .CodeMirror-lines { margin-bottom: 20px; } + +.select2-search--dropdown.select2-search--hide { + display: block; +} diff --git a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js index 806f850f4c..748fb37e18 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -6,12 +6,12 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', - 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', - 'rbacUiControlService', 'ToJSON', 'SourcesService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'Empty', + 'rbacUiControlService', 'ToJSON', 'SourcesService', 'Wait', 'Rest', function($state, $stateParams, $scope, SourcesFormDefinition, ParseTypeChange, GenerateForm, inventoryData, GroupManageService, GetChoices, - GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, - ToJSON, SourcesService) { + GetBasePath, CreateSelect2, GetSourceTypeOptions, Empty, rbacUiControlService, + ToJSON, SourcesService, Wait, Rest) { let form = SourcesFormDefinition; init(); @@ -20,82 +20,157 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', // apply form definition's default field values GenerateForm.applyDefaults(form, $scope); - rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/inventory_sources") - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - $scope.envParseType = 'yaml'; + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/inventory_sources") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + initSources(); } + var getInventoryFiles = function (project) { + var url; + + if (!Empty(project)) { + url = GetBasePath('projects') + project + '/inventories/'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + $scope.inventory_files = data; + $scope.inventory_files.push("/ (project root)"); + sync_inventory_file_select2(); + Wait('stop'); + }) + .error(function (ret,status_code) { + Alert('Cannot get inventory files', 'Unable to retrieve the list of inventory files for this project.', 'alert-info'); + Wait('stop'); + }); + } + }; + + // Detect and alert user to potential SCM status issues + var checkSCMStatus = function () { + if (!Empty($scope.project)) { + Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); + Rest.get() + .success(function (data) { + var msg; + switch (data.status) { + case 'failed': + msg = "
The Project selected has a status of \"failed\". You must run a successful update before you can select an inventory file."; + break; + case 'never updated': + msg = "
The Project selected has a status of \"never updated\". You must run a successful update before you can select an inventory file."; + break; + case 'missing': + msg = '
The selected project has a status of \"missing\". Please check the server and make sure ' + + ' the directory exists and file permissions are set correctly.
'; + break; + } + if (msg) { + Alert('Warning', msg, 'alert-info alert-info--noTextTransform', null, null, null, null, true); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); + }); + } + }; + + // Register a watcher on project_name + if ($scope.getInventoryFilesUnregister) { + $scope.getInventoryFilesUnregister(); + } + $scope.getInventoryFilesUnregister = $scope.$watch('project', function (newValue, oldValue) { + if (newValue !== oldValue) { + getInventoryFiles(newValue); + checkSCMStatus(); + } + }); + + function sync_inventory_file_select2() { + CreateSelect2({ + element:'#inventory-file-select', + addNew: true, + multiple: false, + scope: $scope, + options: 'inventory_files', + model: 'inventory_file' + }); + + // TODO: figure out why the inventory file model is being set to + // dirty + } + $scope.lookupCredential = function(){ - let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; $state.go('.credential', { credential_search: { - kind: kind, + // TODO: get kind sorting for credential properly implemented + // kind: kind, page_size: '5', page: '1' } }); }; - $scope.formCancel = function() { - $state.go('^'); - }; - - $scope.formSave = function() { - var params; - - params = { - name: $scope.name, - description: $scope.description, - inventory: inventoryData.id, - instance_filters: $scope.instance_filters, - source_script: $scope.inventory_script, - credential: $scope.credential, - overwrite: $scope.overwrite, - overwrite_vars: $scope.overwrite_vars, - update_on_launch: $scope.update_on_launch, - update_cache_timeout: $scope.update_cache_timeout || 0, - // comma-delimited strings - group_by: _.map($scope.group_by, 'value').join(','), - source_regions: _.map($scope.source_regions, 'value').join(',') - }; - - if ($scope.source) { - params.source_vars = $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables']; - params.source = $scope.source.value; - } else { - params.source = null; - } - SourcesService.post(params).then(function(res){ - let inventory_source_id = res.data.id; - $state.go('^.edit', {inventory_source_id: inventory_source_id}, {reload: true}); + $scope.lookupProject = function(){ + $state.go('.project', { + project_search: { + page_size: '5', + page: '1' + } }); }; + + $scope.projectBasePath = GetBasePath('projects'); + $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; + $scope.sourceChange = function(source) { - source = source.value; + if (source) { + source = source.value; + } else { + source = ""; + } + + http://localhost:8013/api/v2/credentials/?credential_type__kind__in=cloud,network + + $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; + if (source === 'custom'){ $scope.credentialBasePath = GetBasePath('inventory_script'); } - // equal to case 'ec2' || 'rax' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack' - else{ - $scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source)); - } - if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack') { + + if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm') { + $scope.envParseType = 'yaml'; + + var varName; + if (source === 'scm') { + varName = 'custom_variables'; + } else { + varName = source + '_variables'; + } + ParseTypeChange({ scope: $scope, - field_id: source + '_variables', - variable: source + '_variables', + field_id: varName, + variable: varName, parse_variable: 'envParseType' }); } + if (source === 'scm') { + $scope.overwrite_vars = true; + } else { + $scope.overwrite_vars = false; + } + // reset fields $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint $scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions']; - $scope.cloudCredentialRequired = source !== '' && source !== 'custom' && source !== 'ec2' ? true : false; + $scope.cloudCredentialRequired = source !== '' && source !== 'scm' && source !== 'custom' && source !== 'ec2' ? true : false; $scope.group_by = null; $scope.source_regions = null; $scope.credential = null; @@ -107,6 +182,10 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', initRegionSelect(); }); + $scope.$on('choicesReadyVerbosity', function() { + initVerbositySelect(); + }); + $scope.$on('sourceTypeOptionsReady', function() { initSourceSelect(); }); @@ -121,6 +200,7 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', multiple: true }); } + function initSourceSelect(){ CreateSelect2({ element: '#inventory_source_source', @@ -128,6 +208,15 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', }); } + function initVerbositySelect(){ + CreateSelect2({ + element: '#inventory_source_verbosity', + multiple: false + }); + + $scope.verbosity = $scope.verbosity_options[0]; + } + function initSources(){ GetChoices({ scope: $scope, @@ -174,11 +263,66 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', choice_name: 'ec2_group_by_choices', callback: 'choicesReadyGroup' }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'verbosity', + variable: 'verbosity_options', + callback: 'choicesReadyVerbosity' + }); + GetSourceTypeOptions({ scope: $scope, variable: 'source_type_options', //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref }); } + + $scope.formCancel = function() { + $state.go('^'); + }; + + $scope.formSave = function() { + var params; + + params = { + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id, + instance_filters: $scope.instance_filters, + source_script: $scope.inventory_script, + credential: $scope.credential, + overwrite: $scope.overwrite, + overwrite_vars: $scope.overwrite_vars, + update_on_launch: $scope.update_on_launch, + verbosity: $scope.verbosity.value, + update_cache_timeout: $scope.update_cache_timeout || 0, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(',') + }; + + if ($scope.source) { + params.source_vars = $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables']; + params.source = $scope.source.value; + if ($scope.source.value === 'scm') { + params.update_on_project_update = $scope.update_on_project_update; + params.source_project = $scope.project; + + if ($scope.inventory_file === '/ (project root)') { + params.source_path = ""; + } else { + params.source_path = $scope.inventory_file; + } + } + } else { + params.source = null; + } + SourcesService.post(params).then(function(res){ + let inventory_source_id = res.data.id; + $state.go('^.edit', {inventory_source_id: inventory_source_id}, {reload: true}); + }); + }; } ]; diff --git a/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js index 98517f9e56..db2aa6b193 100644 --- a/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js @@ -7,11 +7,11 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', - 'inventorySourceData', 'SourcesService', 'inventoryData', + 'inventorySourceData', 'SourcesService', 'inventoryData', 'Empty', 'Wait', 'Rest', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON,ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, - inventorySourceData, SourcesService, inventoryData) { + inventorySourceData, SourcesService, inventoryData, Empty, Wait, Rest) { init(); @@ -20,8 +20,17 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', .then(function(canAdd) { $scope.canAdd = canAdd; }); + // instantiate expected $scope values from inventorySourceData - _.assign($scope, { credential: inventorySourceData.credential }, { overwrite: inventorySourceData.overwrite }, { overwrite_vars: inventorySourceData.overwrite_vars }, { update_on_launch: inventorySourceData.update_on_launch }, { update_cache_timeout: inventorySourceData.update_cache_timeout }, { instance_filters: inventorySourceData.instance_filters }, { inventory_script: inventorySourceData.source_script }); + _.assign($scope, + {credential: inventorySourceData.credential}, + {overwrite: inventorySourceData.overwrite}, + {overwrite_vars: inventorySourceData.overwrite_vars}, + {update_on_launch: inventorySourceData.update_on_launch}, + {update_cache_timeout: inventorySourceData.update_cache_timeout}, + {instance_filters: inventorySourceData.instance_filters}, + {inventory_script: inventorySourceData.source_script}, + {verbosity: inventorySourceData.verbosity}); if (inventorySourceData.credential) { $scope.credential_name = inventorySourceData.summary_fields.credential.name; } @@ -41,6 +50,113 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', initSources(); } + var getInventoryFiles = function (project) { + var url; + + if (!Empty(project)) { + url = GetBasePath('projects') + project + '/inventories/'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + $scope.inventory_files = data; + $scope.inventory_files.push("/ (project root)"); + + if (inventorySourceData.source_path !== "") { + $scope.inventory_file = inventorySourceData.source_path; + if ($scope.inventory_files.indexOf($scope.inventory_file) < 0) { + $scope.inventory_files.push($scope.inventory_file); + } + } else { + $scope.inventory_file = "/ (project root)"; + } + sync_inventory_file_select2(); + Wait('stop'); + }) + .error(function (ret,status_code) { + Alert('Cannot get inventory files', 'Unable to retrieve the list of inventory files for this project.', 'alert-info'); + Wait('stop'); + }); + } + }; + + // Detect and alert user to potential SCM status issues + var checkSCMStatus = function () { + if (!Empty($scope.project)) { + Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); + Rest.get() + .success(function (data) { + var msg; + switch (data.status) { + case 'failed': + msg = "
The Project selected has a status of \"failed\". You must run a successful update before you can select an inventory file."; + break; + case 'never updated': + msg = "
The Project selected has a status of \"never updated\". You must run a successful update before you can select an inventory file."; + break; + case 'missing': + msg = '
The selected project has a status of \"missing\". Please check the server and make sure ' + + ' the directory exists and file permissions are set correctly.
'; + break; + } + if (msg) { + Alert('Warning', msg, 'alert-info alert-info--noTextTransform', null, null, null, null, true); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); + }); + } + }; + + // Register a watcher on project_name + if ($scope.getInventoryFilesUnregister) { + $scope.getInventoryFilesUnregister(); + } + $scope.getInventoryFilesUnregister = $scope.$watch('project', function (newValue, oldValue) { + if (newValue !== oldValue) { + getInventoryFiles(newValue); + checkSCMStatus(); + } + }); + + function sync_inventory_file_select2() { + CreateSelect2({ + element:'#inventory-file-select', + addNew: true, + multiple: false, + scope: $scope, + options: 'inventory_files', + model: 'inventory_file' + }); + + // TODO: figure out why the inventory file model is being set to + // dirty + } + + $scope.lookupCredential = function(){ + $state.go('.credential', { + credential_search: { + // TODO: get kind sorting for credential properly implemented + // kind: kind, + page_size: '5', + page: '1' + } + }); + }; + + $scope.lookupProject = function(){ + $state.go('.project', { + project_search: { + page_size: '5', + page: '1' + } + }); + }; + + $scope.projectBasePath = GetBasePath('projects'); + var initRegionSelect = function() { CreateSelect2({ element: '#inventory_source_source_regions', @@ -89,45 +205,49 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', if ($scope.source) { params.source_vars = $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables']; params.source = $scope.source.value; + if ($scope.source.value === 'scm') { + params.update_on_project_update = $scope.update_on_project_update; + params.source_path = $scope.inventory_file; + } } else { params.source = null; } - // switch (source) { - // no inventory source set, just create a new group - // '' is the value supplied for Manual source type - // case null || '': - SourcesService.put(params).then(() => $state.go('.', null, { reload: true })); - // break; - // // create a new group and create/associate an inventory source - // // equal to case 'rax' || 'ec2' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack' || 'custom' - // default: - // GroupManageService.put(group) - // .then(() => GroupManageService.putInventorySource(params, groupData.related.inventory_source)) - // .then(() => $state.go($state.current, null, { reload: true })); - // break; - // } + + SourcesService + .put(params) + .then(() => $state.go('.', null, { reload: true })); }; $scope.sourceChange = function(source) { $scope.source = source; if (source.value === 'ec2' || source.value === 'custom' || - source.value === 'vmware' || source.value === 'openstack') { - $scope[source.value + '_variables'] = $scope[source.value + '_variables'] === (null || undefined) ? '---' : $scope[source.value + '_variables']; + source.value === 'vmware' || source.value === 'openstack' || + source.value === 'scm') { + + var varName; + if (source === 'scm') { + varName = 'custom_variables'; + } else { + varName = source + '_variables'; + } + + $scope[varName] = $scope[varName] === (null || undefined) ? '---' : $scope[varName]; ParseTypeChange({ scope: $scope, - field_id: source.value + '_variables', - variable: source.value + '_variables', + field_id: varName, + variable: varName, parse_variable: 'envParseType', }); } // reset fields // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint $scope.source_region_choices = source.value === 'azure_rm' ? $scope.azure_regions : $scope[source.value + '_regions']; - $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false; + $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'scm' && source.value !== 'ec2' ? true : false; $scope.group_by = null; $scope.source_regions = null; $scope.credential = null; $scope.credential_name = null; + initRegionSelect(); }; @@ -222,6 +342,41 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', }); } + function initVerbositySelect(){ + CreateSelect2({ + element: '#inventory_source_verbosity', + multiple: false + }); + } + + function sync_verbosity_select2() { + CreateSelect2({ + element:'#inventory_source_verbosity', + multiple: false + }); + } + + $scope.$on('choicesReadyVerbosity', function() { + var i; + for (i = 0; i < $scope.verbosity_options.length; i++) { + if ($scope.verbosity_options[i].value === $scope.verbosity) { + $scope.verbosity = $scope.verbosity_options[i]; + } + } + + initVerbositySelect(); + }); + + $scope.$watch('verbosity', sync_verbosity_select2); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'verbosity', + variable: 'verbosity_options', + callback: 'choicesReadyVerbosity' + }); + // region / source options callback $scope.$on('choicesReadyGroup', function() { if (angular.isObject($scope.source)) { diff --git a/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js index befef8a499..659c1c112a 100644 --- a/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js +++ b/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js @@ -11,7 +11,7 @@ export default .success(function (data) { var i, choices = data.actions.GET.source.choices; for (i = 0; i < choices.length; i++) { - if (choices[i][0] !== 'file') { + if (choices[i][0] !== 'file' && choices[i][0] !== "") { scope[variable].push({ label: choices[i][1], value: choices[i][0] diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js index 0e09051f16..09f78d9980 100644 --- a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -101,11 +101,11 @@ Wait('start'); SourcesService.delete(inventory_source.id).then(() => { $('#prompt-modal').modal('hide'); - // if (parseInt($state.params.source_id) === id) { - // $state.go("sources", null, {reload: true}); - // } else { + if (parseInt($state.params.source_id) === invnetory_source) { + $state.go("sources", null, {reload: true}); + } else { $state.go($state.current.name, null, {reload: true}); - // } + } Wait('stop'); }); }; diff --git a/awx/ui/client/src/inventories/sources/sources.form.js b/awx/ui/client/src/inventories/sources/sources.form.js index af23a8876d..67e0840dea 100644 --- a/awx/ui/client/src/inventories/sources/sources.form.js +++ b/awx/ui/client/src/inventories/sources/sources.form.js @@ -66,16 +66,11 @@ return { ngModel: 'source' }, credential: { - // initializes a default value for this search param - // search params with default values set will not generate user-interactable search tags - search: { - kind: null - }, - label: 'Cloud Credential', + label: 'Credential', type: 'lookup', list: 'CredentialList', basePath: 'credentials', - ngShow: "source && source.value !== '' && source.value !== 'custom'", + ngShow: "source && source.value !== ''", sourceModel: 'credential', sourceField: 'name', ngClick: 'lookupCredential()', @@ -86,6 +81,42 @@ return { ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', watchBasePath: "credentialBasePath" }, + project: { + // initializes a default value for this search param + // search params with default values set will not generate user-interactable search tags + label: 'Project', + type: 'lookup', + list: 'ProjectList', + basePath: 'projects', + ngShow: "source && source.value === 'scm'", + sourceModel: 'project', + sourceField: 'name', + ngClick: 'lookupProject()', + awRequiredWhen: { + reqExpression: "projectRequired", + init: "false" + }, + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + watchBasePath: "projectBasePath" + }, + inventory_file: { + label: i18n._('Inventory File'), + type:'select', + ngOptions: 'file for file in inventory_files track by file', + ngShow: "source && source.value === 'scm'", + ngDisabled: "!(group_obj.summary_fields.user_capabilities.edit || canAdd) || disableInventoryFileBecausePermissionDenied", + id: 'inventory-file-select', + awRequiredWhen: { + reqExpression: "inventoryfilerequired", + init: "true" + }, + column: 1, + awPopOver: "

" + i18n._("Select the inventory file to be synced by this source. You can select from the dropdown or enter a file within the input.") + "

", + dataTitle: i18n._('Inventory File'), + dataPlacement: 'right', + dataContainer: "body", + includeInventoryFileNotFoundError: true + }, source_regions: { label: 'Regions', type: 'select', @@ -162,7 +193,7 @@ return { custom_variables: { id: 'custom_variables', label: 'Environment Variables', //"{{vars_label}}" , - ngShow: "source && source.value=='custom' ", + ngShow: "source && source.value=='custom' || source.value === 'scm'", type: 'textarea', class: 'Form-textAreaLabel Form-formGroup--fullWidth', rows: 6, @@ -249,6 +280,20 @@ return { '

View YAML examples at docs.ansible.com

', dataContainer: 'body' }, + verbosity: { + label: i18n._('Verbosity'), + type: 'select', + ngOptions: 'v.label for v in verbosity_options track by v.value', + ngShow: "source && (source.value !== '' && source.value !== null)", + "default": 0, + required: true, + column: 1, + awPopOver: "

" + i18n._("Control the level of output ansible will produce for inventory source update jobs.") + "

", + dataTitle: i18n._('Verbosity'), + dataPlacement: 'right', + dataContainer: "body", + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + }, checkbox_group: { label: 'Update Options', type: 'checkbox_group', @@ -268,7 +313,7 @@ return { dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: "(!(group_obj.summary_fields.user_capabilities.edit || canAdd))" }, { name: 'overwrite_vars', label: 'Overwrite Variables', @@ -283,7 +328,7 @@ return { dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: "(!(group_obj.summary_fields.user_capabilities.edit || canAdd) || source.value === 'scm')" }, { name: 'update_on_launch', label: 'Update on Launch', @@ -296,6 +341,17 @@ return { dataPlacement: 'right', labelClass: 'checkbox-options', ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, { + name: 'update_on_project_update', + label: 'Update on Project Update', + type: 'checkbox', + ngShow: "source.value === 'scm'", + awPopOver: '

TODO

', + dataTitle: 'Update on Project Update', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }] }, update_cache_timeout: { diff --git a/awx/ui/client/src/inventories/sources/sources.list.js b/awx/ui/client/src/inventories/sources/sources.list.js index 38288bf04e..c1037d2a9c 100644 --- a/awx/ui/client/src/inventories/sources/sources.list.js +++ b/awx/ui/client/src/inventories/sources/sources.list.js @@ -62,7 +62,7 @@ export default { columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', - group_update: { + source_update: { //label: 'Sync', mode: 'all', ngClick: 'updateSource(inventory_source)', @@ -97,7 +97,7 @@ export default { awToolTip: "{{ inventory_source.group_schedule_tooltip }}", ngClass: "inventory_source.scm_type_class", dataPlacement: 'top', - ngShow: "!(inventory_source.summary_fields.inventory_source.source === '')" + ngShow: "!(inventory_source.summary_fields.inventory_source.source === '') && inventory_source.summary_fields.user_capabilities.schedule" }, edit: { //label: 'Edit', diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js index 9d03e69d3a..52dae13757 100644 --- a/awx/ui/client/src/projects/projects.list.js +++ b/awx/ui/client/src/projects/projects.list.js @@ -17,6 +17,7 @@ export default ['i18n', function(i18n) { 'Select button, located bottom right.

Create a new project by clicking the button.

', index: false, hover: true, + emptyListText: i18n._('No Projects Have Been Created'), fields: { status: { diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 5a49d1e8ff..73ab69cbdd 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -618,9 +618,13 @@ angular.module('Utilities', ['RestServices', 'Utilities']) var element = params.element, options = params.opts, multiple = (params.multiple !== undefined) ? params.multiple : true, + createNew = (params.createNew !== undefined) ? params.createNew : false, placeholder = params.placeholder, customDropdownAdapter = (params.customDropdownAdapter !== undefined) ? params.customDropdownAdapter : true, - addNew = params.addNew; + addNew = params.addNew, + scope = params.scope, + options = params.options, + model = params.model; $.fn.select2.amd.require([ 'select2/utils', @@ -658,6 +662,10 @@ angular.module('Utilities', ['RestServices', 'Utilities']) if (addNew) { config.tags = true; config.tokenSeparators = []; + + if (!multiple) { + scope["original_" + options] = scope[options]; + } } $(element).select2(config); @@ -677,6 +685,18 @@ angular.module('Utilities', ['RestServices', 'Utilities']) } }).on('select2:unselecting', (e) => { $(e.target).data('select2-unselecting', true); + }) + } + + if (addNew && !multiple) { + $(element).on('select2:select', (e) => { + var opt = $(e.target).find("[data-select2-tag='true']"); + if (opt.length) { + scope[model] = e.params.data.id; + scope[options] = scope["original_" + options]; + scope[options].push($(opt[0]).attr("value")); + } + $(element).trigger('change'); }); } diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index dc7c9a4011..6589d3b555 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -515,6 +515,10 @@ function(ConfigurationUtils, i18n, $rootScope) { _doAutoPopulate(); } }); + + if (attrs.watchbasepath === 'projectBasePath') { + _doAutoPopulate(); + } } function _doAutoPopulate() { @@ -522,7 +526,11 @@ function(ConfigurationUtils, i18n, $rootScope) { if (attrs.watchbasepath !== undefined && scope[attrs.watchbasepath] !== undefined) { basePath = scope[attrs.watchbasepath]; - query = '&role_level=use_role'; + if (attrs.watchbasepath !== "projectBasePath") { + query = '&role_level=use_role'; + } else { + query = ''; + } } else { basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath'); diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index d6c8f03c6d..b46ef824e5 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -132,6 +132,7 @@ angular.module('GeneratorHelpers', [systemStatus.name]) icon = "fa-trash-o"; break; case 'group_update': + case 'source_update': icon = 'fa-refresh'; break; case 'scm_update': From 2b37d042040105b82359b404aed4b26a44c23b41 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 25 May 2017 11:54:26 -0400 Subject: [PATCH 2/6] remove unwanted line --- .../src/inventories/sources/add/sources-add.controller.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js index 748fb37e18..573cd4bbbb 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -134,8 +134,6 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', source = ""; } - http://localhost:8013/api/v2/credentials/?credential_type__kind__in=cloud,network - $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; if (source === 'custom'){ From a2292595592f353aaf0799c413657c81f236b76d Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 25 May 2017 13:04:09 -0400 Subject: [PATCH 3/6] only show text input for add new single-selects --- awx/ui/client/legacy-styles/ansible-ui.less | 4 ---- awx/ui/client/src/shared/Utilities.js | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 7310f1072f..4a7471c060 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -2348,7 +2348,3 @@ input[disabled].ui-spinner-input { .CodeMirror-lines { margin-bottom: 20px; } - -.select2-search--dropdown.select2-search--hide { - display: block; -} diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 73ab69cbdd..8c93cb216d 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -665,6 +665,7 @@ angular.module('Utilities', ['RestServices', 'Utilities']) if (!multiple) { scope["original_" + options] = scope[options]; + config.minimumResultsForSearch = 1; } } From 7f00cfdd5adf69417c14226ab84ee50bf9ea0bbf Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 25 May 2017 13:12:40 -0400 Subject: [PATCH 4/6] set the inventory file to pristine on source change --- .../src/inventories/sources/add/sources-add.controller.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js index 573cd4bbbb..471f2dda7f 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -99,9 +99,6 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', options: 'inventory_files', model: 'inventory_file' }); - - // TODO: figure out why the inventory file model is being set to - // dirty } $scope.lookupCredential = function(){ @@ -160,6 +157,7 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', if (source === 'scm') { $scope.overwrite_vars = true; + $scope.inventory_source_form.inventory_file.$setPristine(); } else { $scope.overwrite_vars = false; } From 6789abbdebfa86bdee095d77d460e21ddbefb6f8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 25 May 2017 14:28:07 -0400 Subject: [PATCH 5/6] smooth out logic for adding new options to single select to select2 --- awx/ui/client/src/shared/Utilities.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 8c93cb216d..16c701e086 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -624,7 +624,12 @@ angular.module('Utilities', ['RestServices', 'Utilities']) addNew = params.addNew, scope = params.scope, options = params.options, - model = params.model; + model = params.model, + original_options; + + if (scope && options) { + original_options = _.cloneDeep(scope[options]); + } $.fn.select2.amd.require([ 'select2/utils', @@ -664,7 +669,6 @@ angular.module('Utilities', ['RestServices', 'Utilities']) config.tokenSeparators = []; if (!multiple) { - scope["original_" + options] = scope[options]; config.minimumResultsForSearch = 1; } } @@ -691,13 +695,12 @@ angular.module('Utilities', ['RestServices', 'Utilities']) if (addNew && !multiple) { $(element).on('select2:select', (e) => { - var opt = $(e.target).find("[data-select2-tag='true']"); - if (opt.length) { - scope[model] = e.params.data.id; - scope[options] = scope["original_" + options]; - scope[options].push($(opt[0]).attr("value")); + scope[model] = e.params.data.text; + scope[options] = _.cloneDeep(original_options); + if (scope[options].indexOf(e.params.data.text) === -1) { + scope[options].push(e.params.data.text); } - $(element).trigger('change'); + $(element).select2(config); }); } From 4fcfac31373cfe4980c35e3dd99a668d33dc09e8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 26 May 2017 11:56:14 -0400 Subject: [PATCH 6/6] updates to jshint and groups language to inventory_source --- .../sources/add/sources-add.controller.js | 11 +++--- .../sources/edit/sources-edit.controller.js | 16 ++++---- .../sources/list/sources-list.controller.js | 13 ++++--- .../sources/list/sources-list.partial.html | 4 +- .../src/inventories/sources/sources.form.js | 38 +++++++++---------- awx/ui/client/src/shared/Utilities.js | 15 ++++---- 6 files changed, 48 insertions(+), 49 deletions(-) diff --git a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js index 471f2dda7f..870125a87d 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -5,13 +5,12 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', - 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', - 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'Empty', - 'rbacUiControlService', 'ToJSON', 'SourcesService', 'Wait', 'Rest', + 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'Empty', + 'rbacUiControlService', 'ToJSON', 'SourcesService', 'Wait', 'Rest', 'Alert', 'ProcessErrors', function($state, $stateParams, $scope, SourcesFormDefinition, ParseTypeChange, - GenerateForm, inventoryData, GroupManageService, GetChoices, + GenerateForm, inventoryData, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, Empty, rbacUiControlService, - ToJSON, SourcesService, Wait, Rest) { + ToJSON, SourcesService, Wait, Rest, Alert, ProcessErrors) { let form = SourcesFormDefinition; init(); @@ -42,7 +41,7 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', sync_inventory_file_select2(); Wait('stop'); }) - .error(function (ret,status_code) { + .error(function () { Alert('Cannot get inventory files', 'Unable to retrieve the list of inventory files for this project.', 'alert-info'); Wait('stop'); }); diff --git a/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js index db2aa6b193..0c354958cb 100644 --- a/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js @@ -5,13 +5,11 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', - 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupManageService', - 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', - 'inventorySourceData', 'SourcesService', 'inventoryData', 'Empty', 'Wait', 'Rest', + 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'inventorySourceData', 'SourcesService', 'inventoryData', 'Empty', 'Wait', 'Rest', 'Alert', 'ProcessErrors', function($state, $stateParams, $scope, ParseVariableString, - rbacUiControlService, ToJSON,ParseTypeChange, GroupManageService, - GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, - inventorySourceData, SourcesService, inventoryData, Empty, Wait, Rest) { + rbacUiControlService, ToJSON,ParseTypeChange, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, + inventorySourceData, SourcesService, inventoryData, Empty, Wait, Rest, Alert, ProcessErrors) { init(); @@ -31,6 +29,8 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', {instance_filters: inventorySourceData.instance_filters}, {inventory_script: inventorySourceData.source_script}, {verbosity: inventorySourceData.verbosity}); + + $scope.inventory_source_obj = inventorySourceData; if (inventorySourceData.credential) { $scope.credential_name = inventorySourceData.summary_fields.credential.name; } @@ -73,7 +73,7 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', sync_inventory_file_select2(); Wait('stop'); }) - .error(function (ret,status_code) { + .error(function () { Alert('Cannot get inventory files', 'Unable to retrieve the list of inventory files for this project.', 'alert-info'); Wait('stop'); }); @@ -104,7 +104,7 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', } }) .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); }); } diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js index 09f78d9980..0f54008553 100644 --- a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -5,17 +5,18 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'SourcesListDefinition', - 'InventoryUpdate', 'GroupManageService', 'CancelSourceUpdate', + 'InventoryUpdate', 'CancelSourceUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', 'GetSyncStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', '$filter', 'Prompt', 'Wait', 'SourcesService', function($scope, $rootScope, $state, $stateParams, SourcesListDefinition, - InventoryUpdate, GroupManageService, CancelSourceUpdate, + InventoryUpdate, CancelSourceUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, GetSyncStatusMsg, Dataset, Find, qs, inventoryData, $filter, Prompt, Wait, SourcesService){ let list = SourcesListDefinition; + var inventory_source; init(); @@ -24,7 +25,7 @@ $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; $scope.canAdd = false; - rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/inventory_sources") .then(function(canAdd) { $scope.canAdd = canAdd; }); @@ -38,7 +39,7 @@ _.forEach($scope[list.name], buildStatusIndicators); $scope.$on(`ws-jobs`, function(e, data){ - var inventory_source = Find({ list: $scope.inventory_sources, key: 'id', val: data.inventory_source_id }); + inventory_source = Find({ list: $scope.inventory_sources, key: 'id', val: data.inventory_source_id }); if (inventory_source === undefined || inventory_source === null) { inventory_source = {}; @@ -101,7 +102,7 @@ Wait('start'); SourcesService.delete(inventory_source.id).then(() => { $('#prompt-modal').modal('hide'); - if (parseInt($state.params.source_id) === invnetory_source) { + if (parseInt($state.params.source_id) === inventory_source) { $state.go("sources", null, {reload: true}); } else { $state.go($state.current.name, null, {reload: true}); @@ -136,7 +137,7 @@ }); }; $scope.scheduleSource = function(id) { - // Add this group's id to the array of group id's so that it gets + // Add this inv source's id to the array of inv source id's so that it gets // added to the breadcrumb trail $state.go('inventories.edit.inventory_sources.edit.schedules', {inventory_source_id: id}, {reload: true}); }; diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.partial.html b/awx/ui/client/src/inventories/sources/list/sources-list.partial.html index 1a02f3a515..03cc6ab1fd 100644 --- a/awx/ui/client/src/inventories/sources/list/sources-list.partial.html +++ b/awx/ui/client/src/inventories/sources/list/sources-list.partial.html @@ -5,7 +5,7 @@