diff --git a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js index 818e1890d2..7b995e7f6a 100644 --- a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -31,195 +31,32 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', field_id: 'group_variables', variable: 'variables', }); - initSources(); } - $scope.lookupCredential = function(){ - let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; - $state.go('.credential', { - credential_search: { - kind: kind, - page_size: '5', - page: '1' - } - }); - }; - $scope.formCancel = function() { $state.go('^'); }; $scope.formSave = function() { - var params, source, json_data; + var json_data; json_data = ToJSON($scope.parseType, $scope.variables, true); - // group fields + var group = { variables: json_data, name: $scope.name, description: $scope.description, inventory: inventoryData.id }; - if ($scope.source) { - // inventory_source fields - params = { - instance_filters: $scope.instance_filters, - source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'], - source_script: $scope.inventory_script, - source: $scope.source.value, - 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(',') - }; - source = $scope.source.value; - } else { - source = null; - } - switch (source) { - // no inventory source set, just create a new group - // '' is the value supplied for Manual source type - case null || '': - GroupManageService.post(group).then(res => { - // associate - if ($stateParams.group) { - return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) - .then(() => $state.go('^', null, { reload: true })); - } else { - $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.post(group) - // associate to group - .then(res => { - if ($stateParams.group) { - GroupManageService.associateGroup(res.data, _.last($stateParams.group)); - return res; - } else { - return res; } - // pass the original POST response and not the association response - }) - .then(res => GroupManageService.putInventorySource( - // put the received group ID into inventory source payload - // and pass the related endpoint - _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) - .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); - break; - } + + GroupManageService.post(group).then(res => { + if ($stateParams.group) { + return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) + .then(() => $state.go('^', null, { reload: true })); + } else { + $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true }); + } + }); + }; - $scope.sourceChange = function(source) { - source = source.value; - 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') { - ParseTypeChange({ - scope: $scope, - field_id: source + '_variables', - variable: source + '_variables', - parse_variable: 'envParseType' - }); - } - - // 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.group_by = null; - $scope.source_regions = null; - $scope.credential = null; - $scope.credential_name = null; - initRegionSelect(); - }; - // region / source options callback - $scope.$on('choicesReadyGroup', function() { - initRegionSelect(); - }); - - $scope.$on('sourceTypeOptionsReady', function() { - initSourceSelect(); - }); - - function initRegionSelect(){ - CreateSelect2({ - element: '#group_source_regions', - multiple: true - }); - CreateSelect2({ - element: '#group_group_by', - multiple: true - }); - } - function initSourceSelect(){ - CreateSelect2({ - element: '#group_source', - multiple: false - }); - } - - function initSources(){ - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'rax_regions', - choice_name: 'rax_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'ec2_regions', - choice_name: 'ec2_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'gce_regions', - choice_name: 'gce_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'azure_regions', - choice_name: 'azure_region_choices', - callback: 'choicesReadyGroup' - }); - - // Load options for group_by - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'group_by', - variable: 'ec2_group_by', - choice_name: 'ec2_group_by_choices', - callback: 'choicesReadyGroup' - }); - GetSourceTypeOptions({ - scope: $scope, - variable: 'source_type_options', - //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref - }); - } } ]; diff --git a/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js index 1355d5d33f..27f444d69a 100644 --- a/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', - 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', + 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON, - ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData) { + ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData) { init(); @@ -16,17 +16,8 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbac .then(function(canAdd) { $scope.canAdd = canAdd; }); - // instantiate expected $scope values from inventorySourceData & groupData - _.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 }); - if (inventorySourceData.credential) { - $scope.credential_name = inventorySourceData.summary_fields.credential.name; - } - $scope = angular.extend($scope, groupData); - // display custom inventory_script name - if (inventorySourceData.source === 'custom') { - $scope.inventory_script_name = inventorySourceData.summary_fields.source_script.name; - } + $scope = angular.extend($scope, groupData); $scope.$watch('summary_fields.user_capabilities.edit', function(val) { $scope.canAdd = val; @@ -43,36 +34,14 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbac variable: 'variables', }); - initSources(); } - var initRegionSelect = function() { - CreateSelect2({ - element: '#group_source_regions', - multiple: true - }); - CreateSelect2({ - element: '#group_group_by', - multiple: true - }); - }; - - $scope.lookupCredential = function(){ - let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; - $state.go('.credential', { - credential_search: { - kind: kind, - page_size: '5', - page: '1' - } - }); - }; - $scope.formCancel = function() { $state.go('^'); }; + $scope.formSave = function() { - var params, source, json_data; + var json_data; json_data = ToJSON($scope.parseType, $scope.variables, true); // group fields var group = { @@ -82,166 +51,8 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbac inventory: $scope.inventory, id: groupData.id }; - if ($scope.source) { - // inventory_source fields - params = { - group: groupData.id, - source: $scope.source.value, - credential: $scope.credential, - overwrite: $scope.overwrite, - overwrite_vars: $scope.overwrite_vars, - source_script: $scope.inventory_script, - 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(','), - instance_filters: $scope.instance_filters, - source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'] - }; - source = $scope.source.value; - } else { - source = null; - } - switch (source) { - // no inventory source set, just create a new group - // '' is the value supplied for Manual source type - case null || '': - GroupManageService.put(group).then(() => $state.go($state.current, 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; - } + GroupManageService.put(group).then(() => $state.go($state.current, 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']; - ParseTypeChange({ - scope: $scope, - field_id: source.value + '_variables', - variable: source.value + '_variables', - 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.group_by = null; - $scope.source_regions = null; - $scope.credential = null; - $scope.credential_name = null; - initRegionSelect(); - }; - - function initSourceSelect() { - $scope.source = _.find($scope.source_type_options, { value: inventorySourceData.source }); - CreateSelect2({ - element: '#group_source', - multiple: false - }); - // After the source is set, conditional fields will be visible - // CodeMirror is buggy if you instantiate it in a not-visible element - // So we initialize it here instead of the init() routine - if (inventorySourceData.source === 'ec2' || inventorySourceData.source === 'openstack' || - inventorySourceData.source === 'custom' || inventorySourceData.source === 'vmware') { - $scope[inventorySourceData.source + '_variables'] = inventorySourceData.source_vars === null || inventorySourceData.source_vars === '' ? '---' : ParseVariableString(inventorySourceData.source_vars); - ParseTypeChange({ - scope: $scope, - field_id: inventorySourceData.source + '_variables', - variable: inventorySourceData.source + '_variables', - parse_variable: 'envParseType', - }); - } - } - - function initRegionData() { - var source = $scope.source.value === 'azure_rm' ? 'azure' : $scope.source.value; - var regions = inventorySourceData.source_regions.split(','); - // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint - $scope.source_region_choices = $scope[source + '_regions']; - - // the API stores azure regions as all-lowercase strings - but the azure regions received from OPTIONS are Snake_Cased - if (source === 'azure') { - $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value.toLowerCase() === region)); - } - // all other regions are 1-1 - else { - $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value === region)); - } - $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; - if (source === 'ec2') { - var group_by = inventorySourceData.group_by.split(','); - $scope.group_by = _.map(group_by, (item) => _.find($scope.ec2_group_by, { value: item })); - } - initRegionSelect(); - } - - function initSources() { - GetSourceTypeOptions({ - scope: $scope, - variable: 'source_type_options', - //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref - }); - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'rax_regions', - choice_name: 'rax_region_choices', - callback: 'choicesReadyGroup' - }); - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'ec2_regions', - choice_name: 'ec2_region_choices', - callback: 'choicesReadyGroup' - }); - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'gce_regions', - choice_name: 'gce_region_choices', - callback: 'choicesReadyGroup' - }); - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'azure_regions', - choice_name: 'azure_region_choices', - callback: 'choicesReadyGroup' - }); - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'group_by', - variable: 'ec2_group_by', - choice_name: 'ec2_group_by_choices', - callback: 'choicesReadyGroup' - }); - } - - // region / source options callback - $scope.$on('choicesReadyGroup', function() { - if (angular.isObject($scope.source)) { - initRegionData(); - } - }); - - $scope.$on('sourceTypeOptionsReady', function() { - initSourceSelect(); - }); } ]; diff --git a/awx/ui/client/src/inventories/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js index bdf9df2353..5199e7bb34 100644 --- a/awx/ui/client/src/inventories/groups/groups.form.js +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -11,329 +11,69 @@ */ export default { + addTitle: 'CREATE GROUP', + editTitle: '{{ name }}', + showTitle: true, + name: 'group', + basePath: 'groups', + parent: 'inventories.edit.groups', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventories', + // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab + // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit + activeEditState: 'inventories.edit.groups.editGroup', + detailsClick: "$state.go('inventories.edit.groups.editGroup')", + well: false, + fields: { + name: { + label: 'Name', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + required: true, + tab: 'properties' + }, + description: { + label: 'Description', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + tab: 'properties' + }, + variables: { + label: 'Variables', + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + dataTitle: 'Group Variables', + dataPlacement: 'right', + parseTypeName: 'parseType', + awPopOver: "

Variables defined here apply to all child groups and hosts.

" + + "

Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body', + tab: 'properties' + } + }, - - addTitle: 'CREATE GROUP', - editTitle: '{{ name }}', - showTitle: true, - name: 'group', - basePath: 'groups', - parent: 'inventories.edit.groups', - // the parent node this generated state definition tree expects to attach to - stateTree: 'inventories', - // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab - // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit - activeEditState: 'inventories.edit.groups.editGroup', - detailsClick: "$state.go('inventories.edit.groups.editGroup')", - well: false, - fields: { - name: { - label: 'Name', - type: 'text', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - required: true, - tab: 'properties' - }, - description: { - label: 'Description', - type: 'text', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - tab: 'properties' - }, - variables: { - label: 'Variables', - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - dataTitle: 'Group Variables', - dataPlacement: 'right', - parseTypeName: 'parseType', - awPopOver: "

Variables defined here apply to all child groups and hosts.

" + - "

Enter variables using either JSON or YAML syntax. Use the " + - "radio button to toggle between the two.

" + - "JSON:
\n" + - "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', - dataContainer: 'body', - tab: 'properties' - }, - source: { - label: 'Source', - type: 'select', - ngOptions: 'source.label for source in source_type_options track by source.value', - ngChange: 'sourceChange(source)', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - 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', - type: 'lookup', - list: 'CredentialList', - basePath: 'credentials', - ngShow: "source && source.value !== '' && source.value !== 'custom'", - sourceModel: 'credential', - sourceField: 'name', - ngClick: 'lookupCredential()', - awRequiredWhen: { - reqExpression: "cloudCredentialRequired", - init: "false" - }, - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - watchBasePath: "credentialBasePath" - }, - source_regions: { - label: 'Regions', - type: 'select', - ngOptions: 'source.label for source in source_region_choices track by source.value', - multiSelect: true, - ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')", - - - dataTitle: 'Source Regions', - dataPlacement: 'right', - awPopOver: "

Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " + - "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + - "

", - dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - instance_filters: { - label: 'Instance Filters', - type: 'text', - ngShow: "source && source.value == 'ec2'", - dataTitle: 'Instance Filters', - dataPlacement: 'right', - awPopOver: "

Provide a comma-separated list of filter expressions. " + - "Hosts are imported to Tower when ANY of the filters match.

" + - "Limit to hosts having a tag:
\n" + - "
tag-key=TowerManaged
\n" + - "Limit to hosts using either key pair:
\n" + - "
key-name=staging, key-name=production
\n" + - "Limit to hosts where the Name tag begins with test:
\n" + - "
tag:Name=test*
\n" + - "

View the Describe Instances documentation " + - "for a complete list of supported filters.

", - dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - group_by: { - label: 'Only Group By', - type: 'select', - ngShow: "source && source.value == 'ec2'", - ngOptions: 'source.label for source in group_by_choices track by source.value', - multiSelect: true, - dataTitle: 'Only Group By', - dataPlacement: 'right', - awPopOver: "

Select which groups to create automatically. " + - "Tower will create group names similar to the following examples based on the options selected:

If blank, all groups above are created except Instance ID.

", - dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - inventory_script: { - label : "Custom Inventory Script", - type: 'lookup', - basePath: 'inventory_scripts', - list: 'InventoryScriptsList', - ngShow: "source && source.value === 'custom'", - sourceModel: 'inventory_script', - sourceField: 'name', - awRequiredWhen: { - reqExpression: "source && source.value === 'custom'", - init: "false" - }, - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - }, - custom_variables: { - id: 'custom_variables', - label: 'Environment Variables', //"{{vars_label}}" , - ngShow: "source && source.value=='custom' ", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Environment Variables", - dataPlacement: 'right', - awPopOver: "

Provide environment variables to pass to the custom inventory script.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', - dataContainer: 'body' - }, - ec2_variables: { - id: 'ec2_variables', - label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'ec2'", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Source Variables", - dataPlacement: 'right', - awPopOver: "

Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + - "" + - "view ec2.ini in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', - dataContainer: 'body' - }, - vmware_variables: { - id: 'vmware_variables', - label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'vmware'", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Source Variables", - dataPlacement: 'right', - awPopOver: "

Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables " + - "" + - "view vmware_inventory.ini in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', - dataContainer: 'body' - }, - openstack_variables: { - id: 'openstack_variables', - label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'openstack'", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Source Variables", - dataPlacement: 'right', - awPopOver: "

Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + - "" + - "view openstack.yml in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', - dataContainer: 'body' - }, - checkbox_group: { - label: 'Update Options', - type: 'checkbox_group', - ngShow: "source && (source.value !== '' && source.value !== null)", - class: 'Form-checkbox--stacked', - fields: [{ - name: 'overwrite', - label: 'Overwrite', - type: 'checkbox', - ngShow: "source.value !== '' && source.value !== null", - - - awPopOver: '

If checked, all child groups and hosts not found on the external source will be deleted from ' + - 'the local inventory.

When not checked, local child hosts and groups not found on the external source will ' + - 'remain untouched by the inventory update process.

', - dataTitle: 'Overwrite', - dataContainer: 'body', - dataPlacement: 'right', - labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, { - name: 'overwrite_vars', - label: 'Overwrite Variables', - type: 'checkbox', - ngShow: "source.value !== '' && source.value !== null", - - - awPopOver: '

If checked, all variables for child groups and hosts will be removed and replaced by those ' + - 'found on the external source.

When not checked, a merge will be performed, combining local variables with ' + - 'those found on the external source.

', - dataTitle: 'Overwrite Variables', - dataContainer: 'body', - dataPlacement: 'right', - labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, { - name: 'update_on_launch', - label: 'Update on Launch', - type: 'checkbox', - ngShow: "source.value !== '' && source.value !== null", - awPopOver: '

Each time a job runs using this inventory, refresh the inventory from the selected source before ' + - 'executing job tasks.

', - dataTitle: 'Update on Launch', - dataContainer: 'body', - dataPlacement: 'right', - labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }] - }, - update_cache_timeout: { - label: "Cache Timeout (seconds)", - id: 'source-cache-timeout', - type: 'number', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - integer: true, - min: 0, - ngShow: "source && source.value !== '' && update_on_launch", - spinner: true, - "default": 0, - awPopOver: '

Time in seconds to consider an inventory sync to be current. During job runs and callbacks the task system will ' + - 'evaluate the timestamp of the latest sync. If it is older than Cache Timeout, it is not considered current, ' + - 'and a new inventory sync will be performed.

', - dataTitle: 'Cache Timeout', - dataPlacement: 'right', - dataContainer: "body" - } - }, - - buttons: { - cancel: { - ngClick: 'formCancel()', - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - close: { - ngClick: 'formCancel()', - ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - save: { - ngClick: 'formSave()', - ngDisabled: true, - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' - } - } - }; + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } + } +}; diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index 0536161ab9..20baec28b1 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -17,19 +17,6 @@ export default { basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/', fields: { - sync_status: { - label: '', - nosort: true, - mode: 'all', - iconOnly: true, - ngClick: 'viewUpdateStatus(group.id)', - awToolTip: "{{ group.status_tooltip }}", - dataTipWatch: "group.status_tooltip", - icon: "{{ 'fa icon-cloud-' + group.status_class }}", - ngClass: "group.status_class", - dataPlacement: "top", - columnClass: 'status-column List-staticColumn--smallStatus' - }, failed_hosts: { label: '', nosort: true, diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index a9db830841..2889b745f9 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -50,13 +50,8 @@ group = {}; } - let group_status, hosts_status; + let hosts_status; - group_status = GetSyncStatusMsg({ - status: group.summary_fields.inventory_source.status, - has_inventory_sources: group.has_inventory_sources, - source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null ) - }); hosts_status = GetHostsStatusMsg({ active_failures: group.hosts_with_active_failures, total_hosts: group.total_hosts, @@ -64,15 +59,8 @@ group_id: group.id }); _.assign(group, - {status_class: group_status.class}, - {status_tooltip: group_status.tooltip}, - {launch_tooltip: group_status.launch_tip}, - {launch_class: group_status.launch_class}, - {group_schedule_tooltip: group_status.schedule_tip}, {hosts_status_tip: hosts_status.tooltip}, - {hosts_status_class: hosts_status.class}, - {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null}, - {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null}); + {hosts_status_class: hosts_status.class}); } $scope.groupSelect = function(id){ diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 875d23486b..032a4ec352 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,6 +1,7 @@
+
diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index 317800d0fb..b59b2ebe39 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -12,9 +12,11 @@ export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', 'buildGroupsEditState', 'buildHostListState', 'buildHostAddState', - 'buildHostEditState', + 'buildHostEditState', 'buildSourcesListState', 'buildSourcesAddState', + 'buildSourcesEditState', function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, - buildHostListState, buildHostAddState, buildHostEditState) { + buildHostListState, buildHostAddState, buildHostEditState, + buildSourcesListState, buildSourcesAddState,buildSourcesEditState) { return { addTitle: i18n._('NEW INVENTORY'), @@ -155,35 +157,13 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, }, inventory_sources: { name: 'inventory_sources', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/inventory_sources/', - type: 'collection', + include: "SourcesListDefinition", + includeForm: "SourcesFormDefinition", title: i18n._('Sources'), iterator: 'inventory_source', - index: false, - open: false, - // search: { - // order_by: 'username' - // }, - actions: { - add: { - label: i18n._('Add'), - ngClick: "$state.go('.add')", - awToolTip: i18n._('Add a permission'), - actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD', - // ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - - } - }, - fields: { - name: { - label: i18n._('Name'), - // linkBase: 'users', - class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' - } - } + listState: buildSourcesListState, + addState: buildSourcesAddState, + editState: buildSourcesEditState }, //this is a placeholder for when we're ready for completed jobs completed_jobs: { diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index c9de579972..74a3af0e1b 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -6,6 +6,7 @@ import host from './hosts/main'; import group from './groups/main'; +import sources from './sources/main'; import relatedHost from './related-hosts/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; @@ -19,6 +20,7 @@ export default angular.module('inventory', [ host.name, group.name, + sources.name, relatedHost.name, inventoryAdd.name, inventoryEdit.name, diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.route.js b/awx/ui/client/src/inventories/manage/inventory-manage.route.js deleted file mode 100644 index ca7589a3e1..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.route.js +++ /dev/null @@ -1,141 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; -import InventoriesManage from './inventory-manage.controller'; -import BreadcrumbsController from './breadcrumbs/breadcrumbs.controller'; -import HostsListController from './hosts/hosts-list.controller'; -import GroupsListController from './groups/groups-list.controller'; - -export default { - name: 'inventoryManage', - data: { - socket: { - "groups": { - "jobs": ["status_changed"] - } - } - }, - // instead of a single 'searchPrefix' attribute, provide hard-coded search params - url: '/inventories/:inventory_id/manage?{group:int}{group_search:queryset}{host_search:queryset}', - params: { - group: { - array: true - }, - group_search: { - value: { - page_size: '20', - page: '1', - order_by: 'name', - }, - squash: true, - dynamic: true - }, - host_search: { - value: { - page_size: '20', - page: '1', - order_by: 'name', - }, - squash: true, - dynamic: true - } - }, - ncyBreadcrumb: { - skip: true // Never display this state in ncy-breadcrumb. - }, - // enforce uniqueness in group param - onEnter: function($stateParams) { - $stateParams.group = _.uniq($stateParams.group); - }, - resolve: { - groupsUrl: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return $stateParams.group && $stateParams.group.length > 0 ? - // nested context - provide this node's children - InventoryManageService.childGroupsUrl(_.last($stateParams.group)) : - // root context - provide root nodes - InventoryManageService.rootGroupsUrl($stateParams.inventory_id); - }], - hostsUrl: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return $stateParams.group && $stateParams.group.length > 0 ? - // nested context - provide all hosts managed by nodes - InventoryManageService.childHostsUrl(_.last($stateParams.group)) : - // root context - provide all hosts in an inventory - InventoryManageService.rootHostsUrl($stateParams.inventory_id); - }], - inventoryData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return InventoryManageService.getInventory($stateParams.inventory_id).then(res => res.data); - }], - breadCrumbData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return $stateParams.group && $stateParams.group.length > 0 ? - // nested context - provide breadcrumb data - InventoryManageService.getBreadcrumbs($stateParams.group).then(res => res.data.results) : - // root context - false; - }], - groupsDataset: ['InventoryGroups', 'QuerySet', '$stateParams', 'groupsUrl', (list, qs, $stateParams, groupsUrl) => { - let path = groupsUrl; - return qs.search(path, $stateParams[`${list.iterator}_search`]); - }], - hostsDataset: ['InventoryHosts', 'QuerySet', '$stateParams', 'hostsUrl', (list, qs, $stateParams, hostsUrl) => { - let path = hostsUrl; - return qs.search(path, $stateParams[`${list.iterator}_search`]); - }] - }, - views: { - // target the ui-view with name "groupBreadcrumbs" at the root view - 'groupBreadcrumbs@': { - controller: BreadcrumbsController, - templateUrl: templateUrl('inventories/manage/breadcrumbs/breadcrumbs') - }, - // target the un-named ui-view @ root level - '@': { - templateUrl: templateUrl('inventories/manage/inventory-manage'), - controller: InventoriesManage - }, - // target ui-views with name@inventoryManage state - 'groupsList@inventoryManage': { - templateProvider: function(InventoryGroups, generateList, $templateRequest, $stateParams, GetBasePath) { - let list = _.cloneDeep(InventoryGroups); - if($stateParams && $stateParams.group) { - list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/children'; - } - else { - //reaches here if the user is on the root level group - list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; - } - let html = generateList.build({ - list: list, - mode: 'edit' - }); - html = generateList.wrapPanel(html); - // Include the custom group delete modal template - return $templateRequest(templateUrl('inventories/manage/groups/groups-list')).then((template) => { - return html.concat(template); - }); - }, - controller: GroupsListController - }, - 'hostsList@inventoryManage': { - templateProvider: function(InventoryHosts, generateList, $stateParams, GetBasePath) { - let list = _.cloneDeep(InventoryHosts); - if($stateParams && $stateParams.group) { - list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/all_hosts'; - } - else { - //reaches here if the user is on the root level group - list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts'; - } - let html = generateList.build({ - list: list, - mode: 'edit' - }); - return generateList.wrapPanel(html); - }, - controller: HostsListController - } - } -}; diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.service.js b/awx/ui/client/src/inventories/manage/inventory-manage.service.js deleted file mode 100644 index 710c494aeb..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.service.js +++ /dev/null @@ -1,60 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - export default - ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', - function($rootScope, Rest, GetBasePath, ProcessErrors, Wait){ - return { - // cute abstractions via fn.bind() - url: function(){ - return ''; - }, - error: function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + this.url + '. GET returned: ' + status }); - }, - success: function(data){ - return data; - }, - // data getters - getInventory: function(id){ - Wait('start'); - this.url = GetBasePath('inventory') + id; - Rest.setUrl(this.url); - return Rest.get() - .success(this.success.bind(this)) - .error(this.error.bind(this)) - .finally(Wait('stop')); - }, - getBreadcrumbs: function(groups){ - Wait('start'); - this.url = GetBasePath('groups') + '?' + _.map(groups, function(item){ - return '&or__id=' + item; - }).join(''); - Rest.setUrl(this.url); - return Rest.get() - .success(this.success.bind(this)) - .error(this.error.bind(this)) - .finally(Wait('stop')); - }, - rootHostsUrl: function(id){ - var url = GetBasePath('inventory') + id + '/hosts'; - return url; - }, - childHostsUrl: function(id){ - var url = GetBasePath('groups') + id + '/all_hosts'; - return url; - }, - childGroupsUrl: function(id){ - var url = GetBasePath('groups') + id + '/children'; - return url; - }, - rootGroupsUrl: function(id){ - var url = GetBasePath('inventory') + id+ '/root_groups'; - return url; - } - }; - }]; diff --git a/awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js b/awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js new file mode 100644 index 0000000000..234fa72bf7 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js @@ -0,0 +1,46 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import SourcesAddController from './sources-add.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition, params) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.add`, + url: `/add`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'sourcesForm@inventories': { + templateProvider: function(GenerateForm, SourcesFormDefinition) { + let form = SourcesFormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'add', + related: false + }); + }, + controller: SourcesAddController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/sources/add/main.js b/awx/ui/client/src/inventories/sources/add/main.js new file mode 100644 index 0000000000..134eaf5214 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildSourcesAddState from './build-sources-add-state.factory'; +import controller from './sources-add.controller'; + +export default +angular.module('sourcesAdd', []) + .factory('buildSourcesAddState', buildSourcesAddState) + .controller('SourcesAddController', controller); 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 new file mode 100644 index 0000000000..ecfe09940f --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -0,0 +1,225 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', + 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'rbacUiControlService', 'ToJSON', 'SourcesService', + function($state, $stateParams, $scope, SourcesFormDefinition, ParseTypeChange, + GenerateForm, inventoryData, GroupManageService, GetChoices, + GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, + ToJSON, SourcesService) { + + let form = SourcesFormDefinition; + init(); + + function init() { + // 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.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + field_id: 'inventory_source_variables', + variable: 'variables', + }); + initSources(); + } + + $scope.lookupCredential = function(){ + let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; + $state.go('.credential', { + credential_search: { + kind: kind, + page_size: '5', + page: '1' + } + }); + }; + + $scope.formCancel = function() { + $state.go('^'); + }; + + $scope.formSave = function() { + var params, source, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + if ($scope.source) { + params = { + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id, + instance_filters: $scope.instance_filters, + source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'], + source_script: $scope.inventory_script, + source: $scope.source.value, + 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, + variables: json_data, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(',') + }; + source = $scope.source.value; + } else { + source = null; + } + // switch (source) { + // // no inventory source set, just create a new group + // // '' is the value supplied for Manual source type + // case null || '': + // GroupManageService.post(group).then(res => { + // // associate + // if ($stateParams.group) { + // return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) + // .then(() => $state.go('^', null, { reload: true })); + // } else { + // $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.post(group) + // // associate to group + // .then(res => { + // if ($stateParams.group) { + // GroupManageService.associateGroup(res.data, _.last($stateParams.group)); + // return res; + // } else { + // return res; } + // // pass the original POST response and not the association response + // }) + // .then(res => GroupManageService.putInventorySource( + // // put the received group ID into inventory source payload + // // and pass the related endpoint + // _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) + // .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); + SourcesService.post({params}).then(function(){ + $state.go('.', null, {reload: true}); + }); + // break; + // } + }; + $scope.sourceChange = function(source) { + source = source.value; + 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') { + ParseTypeChange({ + scope: $scope, + field_id: source + '_variables', + variable: source + '_variables', + parse_variable: 'envParseType' + }); + } + + // 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.group_by = null; + $scope.source_regions = null; + $scope.credential = null; + $scope.credential_name = null; + initRegionSelect(); + }; + // region / source options callback + $scope.$on('choicesReadyGroup', function() { + initRegionSelect(); + }); + + $scope.$on('sourceTypeOptionsReady', function() { + initSourceSelect(); + }); + + function initRegionSelect(){ + CreateSelect2({ + element: '#inventory_source_source_regions', + multiple: true + }); + CreateSelect2({ + element: '#inventory_source_group_by', + multiple: true + }); + } + function initSourceSelect(){ + CreateSelect2({ + element: '#inventory_source_source', + multiple: false + }); + } + + function initSources(){ + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'rax_regions', + choice_name: 'rax_region_choices', + callback: 'choicesReadyGroup' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'ec2_regions', + choice_name: 'ec2_region_choices', + callback: 'choicesReadyGroup' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'gce_regions', + choice_name: 'gce_region_choices', + callback: 'choicesReadyGroup' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'azure_regions', + choice_name: 'azure_region_choices', + callback: 'choicesReadyGroup' + }); + + // Load options for group_by + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'group_by', + variable: 'ec2_group_by', + choice_name: 'ec2_group_by_choices', + callback: 'choicesReadyGroup' + }); + GetSourceTypeOptions({ + scope: $scope, + variable: 'source_type_options', + //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref + }); + } + } +]; diff --git a/awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js b/awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js new file mode 100644 index 0000000000..29b988f349 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js @@ -0,0 +1,49 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import SourcesEditController from './sources-edit.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition, params) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.edit`, + url: `/edit/:source_id`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'groupForm@inventories': { + templateProvider: function(GenerateForm, SourcesFormDefinition) { + let form = SourcesFormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'edit', + related: false + }); + }, + controller: SourcesEditController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }], + inventorySourceData: ['$stateParams', 'SourcesService', function($stateParams, SourcesService) { + return SourcesService.get({id: $stateParams.source_id }).then(res => res.data.results[0]); + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/sources/edit/main.js b/awx/ui/client/src/inventories/sources/edit/main.js new file mode 100644 index 0000000000..eb130001d5 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildSourcesEditState from './build-sources-edit-state.factory'; +import controller from './sources-edit.controller'; + +export default +angular.module('sourcesEdit', []) + .factory('buildSourcesEditState', buildSourcesEditState) + .controller('SourcesEditController', controller); 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 new file mode 100644 index 0000000000..978be62478 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js @@ -0,0 +1,248 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'ParseVariableString', + 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'inventorySourceData', 'SourcesService', + function($state, $stateParams, $scope, ParseVariableString, + rbacUiControlService, ToJSON,ParseTypeChange, GroupManageService, + GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, + inventorySourceData, SourcesService) { + + init(); + + function init() { + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/inventory_sources") + .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 }); + if (inventorySourceData.credential) { + $scope.credential_name = inventorySourceData.summary_fields.credential.name; + } + + // display custom inventory_script name + if (inventorySourceData.source === 'custom') { + $scope.inventory_script_name = inventorySourceData.summary_fields.source_script.name; + } + $scope = angular.extend($scope, inventorySourceData); + + $scope.$watch('summary_fields.user_capabilities.edit', function(val) { + $scope.canAdd = val; + }); + + // init codemirror(s) + $scope.variables = $scope.variables === null || $scope.variables === '' ? '---' : ParseVariableString($scope.variables); + $scope.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + + ParseTypeChange({ + scope: $scope, + field_id: 'inventory_source_variables', + variable: 'variables', + }); + + initSources(); + } + + var initRegionSelect = function() { + CreateSelect2({ + element: '#inventory_source_source_regions', + multiple: true + }); + CreateSelect2({ + element: '#inventory_source_group_by', + multiple: true + }); + }; + + $scope.lookupCredential = function(){ + let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; + $state.go('.credential', { + credential_search: { + kind: kind, + page_size: '5', + page: '1' + } + }); + }; + + $scope.formCancel = function() { + $state.go('^'); + }; + $scope.formSave = function() { + var params, source, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + if ($scope.source) { + // inventory_source fields + params = { + id: $scope.id, + variables: json_data, + name: $scope.name, + description: $scope.description, + inventory: $scope.inventory, + source: $scope.source.value, + credential: $scope.credential, + overwrite: $scope.overwrite, + overwrite_vars: $scope.overwrite_vars, + source_script: $scope.inventory_script, + 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(','), + instance_filters: $scope.instance_filters, + source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'] + }; + source = $scope.source.value; + } else { + 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($state.current, 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; + // } + }; + + $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']; + ParseTypeChange({ + scope: $scope, + field_id: source.value + '_variables', + variable: source.value + '_variables', + 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.group_by = null; + $scope.source_regions = null; + $scope.credential = null; + $scope.credential_name = null; + initRegionSelect(); + }; + + function initSourceSelect() { + $scope.source = _.find($scope.source_type_options, { value: inventorySourceData.source }); + CreateSelect2({ + element: '#inventory_source_source', + multiple: false + }); + // After the source is set, conditional fields will be visible + // CodeMirror is buggy if you instantiate it in a not-visible element + // So we initialize it here instead of the init() routine + if (inventorySourceData.source === 'ec2' || inventorySourceData.source === 'openstack' || + inventorySourceData.source === 'custom' || inventorySourceData.source === 'vmware') { + $scope[inventorySourceData.source + '_variables'] = inventorySourceData.source_vars === null || inventorySourceData.source_vars === '' ? '---' : ParseVariableString(inventorySourceData.source_vars); + ParseTypeChange({ + scope: $scope, + field_id: inventorySourceData.source + '_variables', + variable: inventorySourceData.source + '_variables', + parse_variable: 'envParseType', + }); + } + } + + function initRegionData() { + var source = $scope.source.value === 'azure_rm' ? 'azure' : $scope.source.value; + var regions = inventorySourceData.source_regions.split(','); + // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint + $scope.source_region_choices = $scope[source + '_regions']; + + // the API stores azure regions as all-lowercase strings - but the azure regions received from OPTIONS are Snake_Cased + if (source === 'azure') { + $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value.toLowerCase() === region)); + } + // all other regions are 1-1 + else { + $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value === region)); + } + $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; + if (source === 'ec2') { + var group_by = inventorySourceData.group_by.split(','); + $scope.group_by = _.map(group_by, (item) => _.find($scope.ec2_group_by, { value: item })); + } + initRegionSelect(); + } + + function initSources() { + GetSourceTypeOptions({ + scope: $scope, + variable: 'source_type_options', + //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'rax_regions', + choice_name: 'rax_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'ec2_regions', + choice_name: 'ec2_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'gce_regions', + choice_name: 'gce_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'azure_regions', + choice_name: 'azure_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'group_by', + variable: 'ec2_group_by', + choice_name: 'ec2_group_by_choices', + callback: 'choicesReadyGroup' + }); + } + + // region / source options callback + $scope.$on('choicesReadyGroup', function() { + if (angular.isObject($scope.source)) { + initRegionData(); + } + }); + + $scope.$on('sourceTypeOptionsReady', function() { + initSourceSelect(); + }); + } +]; diff --git a/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js new file mode 100644 index 0000000000..19a846c414 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js @@ -0,0 +1,33 @@ +export default + function GetHostsStatusMsg() { + return function(params) { + var active_failures = params.active_failures, + total_hosts = params.total_hosts, + tip, failures, html_class; + + // Return values for use on host status indicator + + if (active_failures > 0) { + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; + html_class = 'error'; + failures = true; + } else { + failures = false; + if (total_hosts === 0) { + // no hosts + tip = "Contains 0 hosts."; + html_class = 'none'; + } else { + // many hosts with 0 failures + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; + html_class = 'success'; + } + } + + return { + tooltip: tip, + failures: failures, + 'class': html_class + }; + }; + } 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 new file mode 100644 index 0000000000..befef8a499 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js @@ -0,0 +1,37 @@ +export default + function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + variable = params.variable; + + if (scope[variable] === undefined) { + scope[variable] = []; + Rest.setUrl(GetBasePath('inventory_sources')); + Rest.options() + .success(function (data) { + var i, choices = data.actions.GET.source.choices; + for (i = 0; i < choices.length; i++) { + if (choices[i][0] !== 'file') { + scope[variable].push({ + label: choices[i][1], + value: choices[i][0] + }); + } + } + scope.cloudCredentialRequired = false; + scope.$emit('sourceTypeOptionsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status + }); + }); + } + }; + } + +GetSourceTypeOptions.$inject = + [ 'Rest', + 'ProcessErrors', + 'GetBasePath' + ]; diff --git a/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js new file mode 100644 index 0000000000..2541abcc27 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js @@ -0,0 +1,77 @@ +export default + function GetSyncStatusMsg(Empty) { + return function(params) { + var status = params.status, + source = params.source, + has_inventory_sources = params.has_inventory_sources, + launch_class = '', + launch_tip = 'Start sync process', + schedule_tip = 'Schedule future inventory syncs', + stat, stat_class, status_tip; + + stat = status; + stat_class = stat; + + switch (status) { + case 'never updated': + stat = 'never'; + stat_class = 'na'; + status_tip = 'Sync not performed. Click to start it now.'; + break; + case 'none': + case 'ok': + case '': + launch_class = 'btn-disabled'; + stat = 'n/a'; + stat_class = 'na'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + break; + case 'canceled': + status_tip = 'Sync canceled. Click to view log.'; + break; + case 'failed': + status_tip = 'Sync failed. Click to view log.'; + break; + case 'successful': + status_tip = 'Sync completed. Click to view log.'; + break; + case 'pending': + status_tip = 'Sync pending.'; + launch_class = "btn-disabled"; + launch_tip = "Sync pending"; + break; + case 'updating': + case 'running': + launch_class = "btn-disabled"; + launch_tip = "Sync running"; + status_tip = "Sync running. Click to view log."; + break; + } + + if (has_inventory_sources && Empty(source)) { + // parent has a source, therefore this group should not have a source + launch_class = "btn-disabled"; + status_tip = 'Managed by an external cloud source.'; + launch_tip = 'Can only be updated by running a sync on the parent group.'; + } + + if (has_inventory_sources === false && Empty(source)) { + launch_class = 'btn-disabled'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + } + + return { + "class": stat_class, + "tooltip": status_tip, + "status": stat, + "launch_class": launch_class, + "launch_tip": launch_tip, + "schedule_tip": schedule_tip + }; + }; + } + +GetSyncStatusMsg.$inject = + [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js new file mode 100644 index 0000000000..1447d0aa1c --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js @@ -0,0 +1,81 @@ +export default + function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { + return function(params) { + var scope = params.scope, + id = params.id, + group = params.group; + + if (scope.removeCancelUpdate) { + scope.removeCancelUpdate(); + } + scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { + // Cancel the update process + Rest.setUrl(url); + Rest.post() + .success(function () { + Wait('stop'); + //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + + // 'Click the button to monitor the status.', 'alert-info'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. POST status: ' + status + }); + }); + }); + + if (scope.removeCheckCancel) { + scope.removeCheckCancel(); + } + scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { + // Check that we have access to cancelling an update + var url = (current_update) ? current_update : last_update; + url += 'cancel/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + if (data.can_cancel) { + scope.$emit('CancelUpdate', url); + //} else { + // Wait('stop'); + // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + + // 'the latest status.', 'alert-info'); + } + else { + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET status: ' + status + }); + }); + }); + + // Cancel the update process + if (Empty(group)) { + group = Find({ list: scope.groups, key: 'id', val: id }); + scope.selected_group_id = group.id; + } + + if (group && (group.status === 'running' || group.status === 'pending')) { + // We found the group, and there is a running update + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status + }); + }); + } + }; + } + +GroupsCancelUpdate.$inject = + [ 'Empty', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js new file mode 100644 index 0000000000..1f3280b51c --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js @@ -0,0 +1,46 @@ +export default + function ViewUpdateStatus($state, Rest, ProcessErrors, Alert, Wait, Empty, Find) { + return function(params) { + var scope = params.scope, + group_id = params.group_id, + group = Find({ list: scope.groups, key: 'id', val: group_id }); + + if (scope.removeSourceReady) { + scope.removeSourceReady(); + } + scope.removeSourceReady = scope.$on('SourceReady', function(e, source) { + + // Get the ID from the correct summary field + var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id; + + $state.go('inventorySyncStdout', {id: update_id}); + + }); + + if (group) { + if (Empty(group.source)) { + // do nothing + } else if (Empty(group.status) || group.status === "never updated") { + Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + + 'clicking the button.
', 'alert-info', null, null, null, null, true); + } else { + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('SourceReady', data); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + ' GET returned status: ' + status }); + }); + } + } + }; + } + +ViewUpdateStatus.$inject = + [ '$state', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Empty', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js b/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js new file mode 100644 index 0000000000..eb124f2842 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js @@ -0,0 +1,75 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import SourcesListController from './sources-list.controller'; +export default ['SourcesListDefinition', '$stateExtender', 'templateUrl', '$injector', + function(SourcesListDefinition, $stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + searchPrefix: `${list.iterator}`, + name: `${formStateDefinition.name}.${list.iterator}s`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + params: { + [list.iterator + '_search']: { + value: { order_by: field.order_by ? field.order_by : 'name' } + }, + }, + views: { + 'related': { + templateProvider: function(SourcesListDefinition, generateList) { + let list = _.cloneDeep(SourcesListDefinition); + let html = generateList.build({ + list: list, + mode: 'edit' + }); + // Include the custom group delete modal template + // return $templateRequest(templateUrl('inventories/groups/list/groups-list')).then((template) => { + // return html.concat(template); + // }); + return html; + }, + controller: SourcesListController + } + }, + resolve: { + ListDefinition: () => { + return list; + }, + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + inventoryData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { + return InventoryManageService.getInventory($stateParams.inventory_id).then(res => res.data); + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + // appy any default search parameters in form definition + // if (field.search) { + // state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); + // } + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/sources/list/main.js b/awx/ui/client/src/inventories/sources/list/main.js new file mode 100644 index 0000000000..c33538ab1f --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildSourcesListState from './build-sources-list-state.factory'; +import controller from './sources-list.controller'; + +export default + angular.module('sourcesList', []) + .factory('buildSourcesListState', buildSourcesListState) + .controller('SourcesListController', controller); 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 new file mode 100644 index 0000000000..21c9003f5b --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -0,0 +1,213 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'SourcesListDefinition', + 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', + 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', + 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', + 'inventoryData', '$filter', 'Prompt', 'Wait', 'SourcesService', + function($scope, $rootScope, $state, $stateParams, SourcesListDefinition, + InventoryUpdate, GroupManageService, GroupsCancelUpdate, + ViewUpdateStatus, rbacUiControlService, GetBasePath, GetSyncStatusMsg, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData, $filter, Prompt, + Wait, SourcesService){ + + let list = SourcesListDefinition; + + init(); + + function init(){ + $scope.inventory_id = $stateParams.inventory_id; + $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; + $scope.canAdd = false; + + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. + // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: + // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the + // problem that this solves. + $scope.ncyBreadcrumbIgnore = true; + if($state.current.name === "inventoryManage.editGroup") { + $scope.rowBeingEdited = $state.params.group_id; + $scope.listBeingEdited = "groups"; + } + + $scope.inventory_id = $stateParams.inventory_id; + _.forEach($scope[list.name], buildStatusIndicators); + + } + + function buildStatusIndicators(inventory_source){ + if (inventory_source === undefined || inventory_source === null) { + inventory_source = {}; + } + + let inventory_source_status, hosts_status; + + inventory_source_status = GetSyncStatusMsg({ + status: inventory_source.status, + has_inventory_sources: inventory_source.has_inventory_sources, + source: ( (inventory_source) ? inventory_source.source : null ) + }); + hosts_status = GetHostsStatusMsg({ + active_failures: inventory_source.hosts_with_active_failures, + total_hosts: inventory_source.total_hosts, + inventory_id: $scope.inventory_id, + // group_id: group.id + }); + _.assign(inventory_source, + {status_class: inventory_source_status.class}, + {status_tooltip: inventory_source_status.tooltip}, + {launch_tooltip: inventory_source_status.launch_tip}, + {launch_class: inventory_source_status.launch_class}, + {group_schedule_tooltip: inventory_source_status.schedule_tip}, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}, + {source: inventory_source ? inventory_source.source : null}, + {status: inventory_source ? inventory_source.status : null}); + } + + $scope.groupSelect = function(id){ + var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); + $state.go('inventoryManage', { + inventory_id: $stateParams.inventory_id, + group: group, + group_search: { + page_size: '20', + page: '1', + order_by: 'name', + } + }, {reload: true}); + }; + $scope.createSource = function(){ + $state.go('inventories.edit.inventory_sources.add'); + }; + $scope.editSource = function(id){ + $state.go('inventories.edit.inventory_sources.edit', {source_id: id}); + }; + $scope.deleteSource = function(inventory_source){ + var body = '
Are you sure you want to permanently delete the inventory source below from the inventory?
' + $filter('sanitize')(inventory_source.name) + '
'; + var action = function(){ + delete $rootScope.promptActionBtnClass; + 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 { + $state.go($state.current.name, null, {reload: true}); + // } + Wait('stop'); + }); + }; + // Prompt depends on having $rootScope.promptActionBtnClass available... + Prompt({ + hdr: 'Delete Source', + body: body, + action: action, + actionText: 'DELETE', + }); + $rootScope.promptActionBtnClass = 'Modal-errorButton'; + }; + + $scope.updateSource = function(inventory_source) { + InventoryUpdate({ + scope: $scope, + url: inventory_source.related.update + }); + }; + + $scope.$on(`ws-jobs`, function(e, data){ + var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + + if (group === undefined || group === null) { + group = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path; + if($stateParams && $stateParams.group && $stateParams.group.length > 0) { + path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + qs.search(path, $state.params[`${list.iterator}_search`]) + .then(function(searchResponse) { + $scope[`${list.iterator}_dataset`] = searchResponse.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + // _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + group.status = data.status; + group.status_class = status.class; + group.status_tooltip = status.tooltip; + group.launch_tooltip = status.launch_tip; + group.launch_class = status.launch_class; + } + }); + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + $scope.showFailedHosts = function() { + $state.go('inventoryManage', {failed: true}, {reload: true}); + }; + $scope.scheduleGroup = function(id) { + // Add this group's id to the array of group id's so that it gets + // added to the breadcrumb trail + var groupsArr = $stateParams.group ? $stateParams.group : []; + groupsArr.push(id); + $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options + $scope.$on('multiSelectList.selectionChanged', (event, selection) => { + $scope.$parent.groupsSelected = selection.length > 0 ? true : false; + $scope.$parent.groupsSelectedItems = selection.selectedItems; + }); + + $scope.copyMoveGroup = function(id){ + $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventoryManage.editGroup") { + $scope.rowBeingEdited = toParams.group_id; + $scope.listBeingEdited = "groups"; + } + else { + delete $scope.rowBeingEdited; + delete $scope.listBeingEdited; + } + }); + + // Remove the listener when the scope is destroyed to avoid a memory leak + $scope.$on('$destroy', function() { + cleanUpStateChangeListener(); + }); + + }]; 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 new file mode 100644 index 0000000000..1a02f3a515 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/sources-list.partial.html @@ -0,0 +1,79 @@ + diff --git a/awx/ui/client/src/inventories/sources/main.js b/awx/ui/client/src/inventories/sources/main.js new file mode 100644 index 0000000000..bb515628b7 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/main.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import sourcesList from './list/main'; +import sourcesAdd from './add/main'; +import sourcesEdit from './edit/main'; +import sourcesFormDefinition from './sources.form'; +import sourcesListDefinition from './sources.list'; +import service from './sources.service'; + +export default + angular.module('sources', [ + sourcesList.name, + sourcesAdd.name, + sourcesEdit.name + ]) + .value('SourcesFormDefinition', sourcesFormDefinition) + .value('SourcesListDefinition', sourcesListDefinition) + .service('SourcesService', service); diff --git a/awx/ui/client/src/inventories/sources/sources.form.js b/awx/ui/client/src/inventories/sources/sources.form.js new file mode 100644 index 0000000000..32968fc3db --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.form.js @@ -0,0 +1,337 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Groups + * @description This form is for adding/editing a Group on the inventory page +*/ + +export default { + addTitle: 'CREATE SOURCE', + editTitle: '{{ name }}', + showTitle: true, + name: 'inventory_source', + basePath: 'inventory_sources', + parent: 'inventories.edit.sources', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventories', + // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab + // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit + activeEditState: 'inventories.edit.groups.editGroup', + detailsClick: "$state.go('inventories.edit.inventory_sources.edit')", + well: false, + fields: { + name: { + label: 'Name', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + required: true, + tab: 'properties' + }, + description: { + label: 'Description', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + tab: 'properties' + }, + variables: { + label: 'Variables', + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + dataTitle: 'Group Variables', + dataPlacement: 'right', + parseTypeName: 'parseType', + awPopOver: "

Variables defined here apply to all child groups and hosts.

" + + "

Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body', + tab: 'properties' + }, + source: { + label: 'Source', + type: 'select', + ngOptions: 'source.label for source in source_type_options track by source.value', + ngChange: 'sourceChange(source)', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + 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', + type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + ngShow: "source && source.value !== '' && source.value !== 'custom'", + sourceModel: 'credential', + sourceField: 'name', + ngClick: 'lookupCredential()', + awRequiredWhen: { + reqExpression: "cloudCredentialRequired", + init: "false" + }, + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + watchBasePath: "credentialBasePath" + }, + source_regions: { + label: 'Regions', + type: 'select', + ngOptions: 'source.label for source in source_region_choices track by source.value', + multiSelect: true, + ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')", + + + dataTitle: 'Source Regions', + dataPlacement: 'right', + awPopOver: "

Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " + + "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + + "

", + dataContainer: 'body', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + instance_filters: { + label: 'Instance Filters', + type: 'text', + ngShow: "source && source.value == 'ec2'", + dataTitle: 'Instance Filters', + dataPlacement: 'right', + awPopOver: "

Provide a comma-separated list of filter expressions. " + + "Hosts are imported to Tower when ANY of the filters match.

" + + "Limit to hosts having a tag:
\n" + + "
tag-key=TowerManaged
\n" + + "Limit to hosts using either key pair:
\n" + + "
key-name=staging, key-name=production
\n" + + "Limit to hosts where the Name tag begins with test:
\n" + + "
tag:Name=test*
\n" + + "

View the Describe Instances documentation " + + "for a complete list of supported filters.

", + dataContainer: 'body', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + group_by: { + label: 'Only Group By', + type: 'select', + ngShow: "source && source.value == 'ec2'", + ngOptions: 'source.label for source in group_by_choices track by source.value', + multiSelect: true, + dataTitle: 'Only Group By', + dataPlacement: 'right', + awPopOver: "

Select which groups to create automatically. " + + "Tower will create group names similar to the following examples based on the options selected:

    " + + "
  • Availability Zone: zones » us-east-1b
  • " + + "
  • Image ID: images » ami-b007ab1e
  • " + + "
  • Instance ID: instances » i-ca11ab1e
  • " + + "
  • Instance Type: types » type_m1_medium
  • " + + "
  • Key Name: keys » key_testing
  • " + + "
  • Region: regions » us-east-1
  • " + + "
  • Security Group: security_groups » security_group_default
  • " + + "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + + "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + + "
  • Tag None: tags » tag_none
  • " + + "

If blank, all groups above are created except Instance ID.

", + dataContainer: 'body', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + inventory_script: { + label : "Custom Inventory Script", + type: 'lookup', + basePath: 'inventory_scripts', + list: 'InventoryScriptsList', + ngShow: "source && source.value === 'custom'", + sourceModel: 'inventory_script', + sourceField: 'name', + awRequiredWhen: { + reqExpression: "source && source.value === 'custom'", + init: "false" + }, + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + }, + custom_variables: { + id: 'custom_variables', + label: 'Environment Variables', //"{{vars_label}}" , + ngShow: "source && source.value=='custom' ", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Environment Variables", + dataPlacement: 'right', + awPopOver: "

Provide environment variables to pass to the custom inventory script.

" + + "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body' + }, + ec2_variables: { + id: 'ec2_variables', + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && source.value == 'ec2'", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", + dataPlacement: 'right', + awPopOver: "

Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + + "" + + "view ec2.ini in the Ansible github repo.

" + + "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body' + }, + vmware_variables: { + id: 'vmware_variables', + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && source.value == 'vmware'", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", + dataPlacement: 'right', + awPopOver: "

Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables " + + "" + + "view vmware_inventory.ini in the Ansible github repo.

" + + "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body' + }, + openstack_variables: { + id: 'openstack_variables', + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && source.value == 'openstack'", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", + dataPlacement: 'right', + awPopOver: "

Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + + "" + + "view openstack.yml in the Ansible github repo.

" + + "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body' + }, + checkbox_group: { + label: 'Update Options', + type: 'checkbox_group', + ngShow: "source && (source.value !== '' && source.value !== null)", + class: 'Form-checkbox--stacked', + fields: [{ + name: 'overwrite', + label: 'Overwrite', + type: 'checkbox', + ngShow: "source.value !== '' && source.value !== null", + + + awPopOver: '

If checked, all child groups and hosts not found on the external source will be deleted from ' + + 'the local inventory.

When not checked, local child hosts and groups not found on the external source will ' + + 'remain untouched by the inventory update process.

', + dataTitle: 'Overwrite', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, { + name: 'overwrite_vars', + label: 'Overwrite Variables', + type: 'checkbox', + ngShow: "source.value !== '' && source.value !== null", + + + awPopOver: '

If checked, all variables for child groups and hosts will be removed and replaced by those ' + + 'found on the external source.

When not checked, a merge will be performed, combining local variables with ' + + 'those found on the external source.

', + dataTitle: 'Overwrite Variables', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, { + name: 'update_on_launch', + label: 'Update on Launch', + type: 'checkbox', + ngShow: "source.value !== '' && source.value !== null", + awPopOver: '

Each time a job runs using this inventory, refresh the inventory from the selected source before ' + + 'executing job tasks.

', + dataTitle: 'Update on Launch', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }] + }, + update_cache_timeout: { + label: "Cache Timeout (seconds)", + id: 'source-cache-timeout', + type: 'number', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + integer: true, + min: 0, + ngShow: "source && source.value !== '' && update_on_launch", + spinner: true, + "default": 0, + awPopOver: '

Time in seconds to consider an inventory sync to be current. During job runs and callbacks the task system will ' + + 'evaluate the timestamp of the latest sync. If it is older than Cache Timeout, it is not considered current, ' + + 'and a new inventory sync will be performed.

', + dataTitle: 'Cache Timeout', + dataPlacement: 'right', + dataContainer: "body" + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } + } +}; diff --git a/awx/ui/client/src/inventories/sources/sources.list.js b/awx/ui/client/src/inventories/sources/sources.list.js new file mode 100644 index 0000000000..cfd714072f --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.list.js @@ -0,0 +1,146 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'inventory_sources', + iterator: 'inventory_source', + editTitle: '{{ inventory_source.name }}', + well: true, + wellOverride: true, + index: false, + hover: true, + multiSelect: true, + trackBy: 'inventory_source.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/inventory_sources/', + + fields: { + sync_status: { + label: '', + nosort: true, + mode: 'all', + iconOnly: true, + ngClick: 'viewUpdateStatus(inventory_source.id)', + awToolTip: "{{ inventory_source.status_tooltip }}", + dataTipWatch: "inventory_source.status_tooltip", + icon: "{{ 'fa icon-cloud-' + inventory_source.status_class }}", + ngClass: "inventory_source.status_class", + dataPlacement: "top", + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + label: 'Sources', + key: true, + ngClick: "groupSelect(inventory_source.id)", + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', + class: 'InventoryManage-breakWord', + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + // launch: { + // mode: 'all', + // // $scope.$parent is governed by InventoryManageController, + // ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', + // ngClick: '$parent.setAdhocPattern()', + // awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", + // dataTipWatch: "adhocCommandTooltip", + // actionClass: 'btn List-buttonDefault', + // buttonContent: 'RUN COMMANDS', + // showTipWhenDisabled: true, + // tooltipInnerClass: "Tooltip-wide", + // ngShow: 'canAdhoc' + // // TODO: set up a tip watcher and change text based on when + // // things are selected/not selected. This is started and + // // commented out in the inventory controller within the watchers. + // // awToolTip: "{{ adhocButtonTipContents }}", + // // dataTipWatch: "adhocButtonTipContents" + // }, + create: { + mode: 'all', + ngClick: "createSource()", + awToolTip: "Create a new source", + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD SOURCE', + ngShow: 'canAdd', + dataPlacement: "top", + } + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', + + group_update: { + //label: 'Sync', + mode: 'all', + ngClick: 'updateSource(inventory_source)', + awToolTip: "{{ inventory_source.launch_tooltip }}", + dataTipWatch: "inventory_source.launch_tooltip", + ngShow: "(inventory_source.status !== 'running' && inventory_source.status " + + "!== 'pending' && inventory_source.status !== 'updating') && inventory_source.summary_fields.user_capabilities.start", + ngClass: "inventory_source.launch_class", + dataPlacement: "top", + }, + cancel: { + //label: 'Cancel', + mode: 'all', + ngClick: "cancelUpdate(inventory_source.id)", + awToolTip: "Cancel sync process", + 'class': 'red-txt', + ngShow: "(inventory_source.status == 'running' || inventory_source.status == 'pending' " + + "|| inventory_source.status == 'updating') && inventory_source.summary_fields.user_capabilities.start", + dataPlacement: "top", + iconClass: "fa fa-minus-circle" + }, + copy: { + mode: 'all', + ngClick: "copyMoveSource(inventory_source.id)", + awToolTip: 'Copy or move source', + ngShow: "inventory_source.id > 0 && inventory_source.summary_fields.user_capabilities.copy", + dataPlacement: "top" + }, + schedule: { + mode: 'all', + ngClick: "scheduleSource(inventory_source.id)", + awToolTip: "{{ inventory_source.group_schedule_tooltip }}", + ngClass: "inventory_source.scm_type_class", + dataPlacement: 'top', + ngShow: "!(inventory_source.summary_fields.inventory_source.source === '')" + }, + edit: { + //label: 'Edit', + mode: 'all', + ngClick: "editSource(inventory_source.id)", + awToolTip: 'Edit source', + dataPlacement: "top", + ngShow: "inventory_source.summary_fields.user_capabilities.edit" + }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editSource(inventory_source.id)", + awToolTip: 'View source', + dataPlacement: "top", + ngShow: "!inventory_source.summary_fields.user_capabilities.edit" + }, + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "deleteSource(inventory_source)", + awToolTip: 'Delete source', + dataPlacement: "top", + ngShow: "inventory_source.summary_fields.user_capabilities.delete" + } + } +}; diff --git a/awx/ui/client/src/inventories/sources/sources.service.js b/awx/ui/client/src/inventories/sources/sources.service.js new file mode 100644 index 0000000000..a913951806 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.service.js @@ -0,0 +1,113 @@ +export default + ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', function($rootScope, Rest, GetBasePath, ProcessErrors, Wait){ + return { + stringifyParams: function(params){ + return _.reduce(params, (result, value, key) => { + return result + key + '=' + value + '&'; + }, ''); + }, + // cute abstractions via fn.bind() + url: function(){ + return ''; + }, + error: function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + this.url + '. GET returned: ' + status }); + }, + success: function(data){ + return data; + }, + // HTTP methods + get: function(params){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + '?' + this.stringifyParams(params); + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + post: function(inventory_source){ + Wait('start'); + this.url = GetBasePath('inventory_sources'); + Rest.setUrl(this.url); + return Rest.post(inventory_source) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + put: function(inventory_source){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + inventory_source.id; + Rest.setUrl(this.url); + return Rest.put(inventory_source) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + delete: function(id){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + id; + Rest.setUrl(this.url); + return Rest.destroy() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + getCredential: function(id){ + Wait('start'); + this.url = GetBasePath('credentials') + id; + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + getInventorySource: function(params){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + '?' + this.stringifyParams(params); + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + putInventorySource: function(params, url){ + Wait('start'); + this.url = url; + Rest.setUrl(this.url); + return Rest.put(params) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + // these relationship setters could be consolidated, but verbosity makes the operation feel more clear @ controller level + associateGroup: function(group, target){ + Wait('start'); + this.url = GetBasePath('groups') + target + '/children/'; + Rest.setUrl(this.url); + return Rest.post(group) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + disassociateGroup: function(group, parent){ + Wait('start'); + this.url = GetBasePath('groups') + parent + '/children/'; + Rest.setUrl(this.url); + return Rest.post({id: group, disassociate: 1}) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + promote: function(group, inventory){ + Wait('start'); + this.url = GetBasePath('inventory') + inventory + '/groups/'; + Rest.setUrl(this.url); + return Rest.post({id: group, disassociate: 1}) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + } + }; + }]; diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index b131ff4c95..785a1414e7 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -246,6 +246,7 @@ function($injector, $stateExtender, $log, i18n) { * @returns {array} Array of state definitions [{...}, {...}, ...] */ generateFormListDefinitions: function(form, formStateDefinition, params) { + var that = this; function buildRbacUserTeamDirective(){ let states = []; @@ -559,11 +560,23 @@ function($injector, $stateExtender, $log, i18n) { states = _.flatten(states); } if(field && field.addState){ - states.push(field.addState(field, formStateDefinition, params)); + let formState = field.addState(field, formStateDefinition, params); + states.push(formState); + // intent here is to add lookup states for any add-forms + if(field.includeForm){ + let form = field.includeForm ? $injector.get(field.includeForm) : field; + states.push(that.generateLookupNodes(form, formState)); + } states = _.flatten(states); } if(field && field.editState){ - states.push(field.editState(field, formStateDefinition, params)); + let formState = field.editState(field, formStateDefinition, params); + states.push(formState); + // intent here is to add lookup states for any edit-forms + if(field.includeForm){ + let form = field.includeForm ? $injector.get(field.includeForm) : field; + states.push(that.generateLookupNodes(form, formState)); + } states = _.flatten(states); } }