diff --git a/awx/ui/static/js/forms/Groups.js b/awx/ui/static/js/forms/Groups.js
index 1569abbc10..ce3631b78b 100644
--- a/awx/ui/static/js/forms/Groups.js
+++ b/awx/ui/static/js/forms/Groups.js
@@ -15,20 +15,7 @@ angular.module('GroupFormDefinition', [])
cancelButton: false,
name: 'group',
well: false,
- formLabelSize: 'col-lg-3',
- formFieldSize: 'col-lg-9',
-
- tabs: [{
- name: 'properties',
- label: 'Properties'
- }, {
- name: 'source',
- label: 'Source'
- },{
- name: 'schedules',
- label: 'Schedules'
- }],
-
+
fields: {
name: {
label: 'Name',
@@ -64,143 +51,10 @@ angular.module('GroupFormDefinition', [])
'
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',
- ngChange: 'sourceChange()',
- addRequired: false,
- editRequired: false,
- //'default': { label: 'Manual', value: '' },
- tab: 'source'
- },
- source_path: {
- label: 'Script Path',
- ngShow: "source && source.value == 'file'",
- type: 'text',
- awRequiredWhen: {
- variable: "sourcePathRequired",
- init: "false"
- },
- tab: 'source'
- },
- credential: {
- label: 'Cloud Credential',
- type: 'lookup',
- ngShow: "source && source.value !== ''",
- sourceModel: 'credential',
- sourceField: 'name',
- ngClick: 'lookUpCredential()',
- addRequired: false,
- editRequired: false,
- tab: 'source'
- },
- source_regions: {
- label: 'Regions',
- type: 'text',
- ngShow: "source && (source.value == 'rax' || source.value == 'ec2')",
- addRequired: false,
- editRequired: false,
- awMultiselect: 'source_region_choices',
- 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',
- tab: 'source'
- },
- source_vars: {
- label: 'Source Variables',
- ngShow: "source && (source.value == 'file' || source.value == 'ec2')",
- type: 'textarea',
- addRequired: false,
- editRequird: false,
- 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',
- tab: 'source'
- },
- checkbox_group: {
- label: 'Update Options',
- type: 'checkbox_group',
- ngShow: "source && (source.value !== '' && source.value !== null)",
- tab: 'source',
-
- fields: [{
- name: 'overwrite',
- label: 'Overwrite',
- type: 'checkbox',
- ngShow: "source.value !== '' && source.value !== null",
- addRequired: false,
- editRequired: false,
- awPopOver: 'When checked all child groups and hosts not found on the remote source will be deleted from ' +
- 'the local inventory.
Unchecked any 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'
- }, {
- name: 'overwrite_vars',
- label: 'Overwrite Variables',
- type: 'checkbox',
- ngShow: "source.value !== '' && source.value !== null",
- addRequired: false,
- editRequired: false,
- 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'
- }, {
- name: 'update_on_launch',
- label: 'Update on Launch',
- type: 'checkbox',
- ngShow: "source.value !== '' && source.value !== null",
- addRequired: false,
- editRequired: false,
- 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'
- }]
}
},
- buttons: {
- /*
- labelClass: 'col-lg-3',
- controlClass: 'col-lg-5',
-
- save: {
- ngClick: 'formSave()',
- ngDisabled: true
- },
- reset: {
- ngClick: 'formReset()',
- ngDisabled: true //Disabled when $pristine
- }
- */
- },
+ buttons: { },
related: { }
diff --git a/awx/ui/static/js/forms/Source.js b/awx/ui/static/js/forms/Source.js
new file mode 100644
index 0000000000..0906f545c1
--- /dev/null
+++ b/awx/ui/static/js/forms/Source.js
@@ -0,0 +1,140 @@
+/*********************************************
+ * Copyright (c) 2014 AnsibleWorks, Inc.
+ *
+ * Groups.js
+ * Form definition for Group model
+ *
+ *
+ */
+angular.module('SourceFormDefinition', [])
+ .value('SourceForm', {
+
+ addTitle: 'Create Source',
+ editTitle: 'Edit Source',
+ showTitle: false,
+ cancelButton: false,
+ name: 'source',
+ well: false,
+
+ fields: {
+ source: {
+ label: 'Source',
+ type: 'select',
+ ngOptions: 'source.label for source in source_type_options',
+ ngChange: 'sourceChange()',
+ addRequired: false,
+ editRequired: false
+ },
+ source_path: {
+ label: 'Script Path',
+ ngShow: "source && source.value == 'file'",
+ type: 'text',
+ awRequiredWhen: {
+ variable: "sourcePathRequired",
+ init: "false"
+ }
+ },
+ credential: {
+ label: 'Cloud Credential',
+ type: 'lookup',
+ ngShow: "source && source.value !== ''",
+ sourceModel: 'credential',
+ sourceField: 'name',
+ ngClick: 'lookUpCredential()',
+ addRequired: false,
+ editRequired: false
+ },
+ source_regions: {
+ label: 'Regions',
+ type: 'text',
+ ngShow: "source && (source.value == 'rax' || source.value == 'ec2')",
+ addRequired: false,
+ editRequired: false,
+ awMultiselect: 'source_region_choices',
+ 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'
+ },
+ source_vars: {
+ label: 'Source Variables',
+ ngShow: "source && (source.value == 'file' || source.value == 'ec2')",
+ type: 'textarea',
+ addRequired: false,
+ editRequird: false,
+ 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'
+ },
+ checkbox_group: {
+ label: 'Update Options',
+ type: 'checkbox_group',
+ ngShow: "source && (source.value !== '' && source.value !== null)",
+
+ fields: [{
+ name: 'overwrite',
+ label: 'Overwrite',
+ type: 'checkbox',
+ ngShow: "source.value !== '' && source.value !== null",
+ addRequired: false,
+ editRequired: false,
+ awPopOver: 'When checked all child groups and hosts not found on the remote source will be deleted from ' +
+ 'the local inventory.
Unchecked any 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'
+ }, {
+ name: 'overwrite_vars',
+ label: 'Overwrite Variables',
+ type: 'checkbox',
+ ngShow: "source.value !== '' && source.value !== null",
+ addRequired: false,
+ editRequired: false,
+ 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'
+ }, {
+ name: 'update_on_launch',
+ label: 'Update on Launch',
+ type: 'checkbox',
+ ngShow: "source.value !== '' && source.value !== null",
+ addRequired: false,
+ editRequired: false,
+ 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'
+ }]
+ }
+ },
+
+ buttons: {
+
+ },
+
+ related: { }
+
+ });
\ No newline at end of file
diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js
index e7d792b32a..de325c82ab 100644
--- a/awx/ui/static/js/helpers/Groups.js
+++ b/awx/ui/static/js/helpers/Groups.js
@@ -12,7 +12,7 @@
angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'GroupListDefinition', 'SearchHelper',
'PaginationHelpers', 'ListGenerator', 'AuthService', 'GroupsHelper', 'InventoryHelper', 'SelectionHelper',
'JobSubmissionHelper', 'RefreshHelper', 'PromptDialog', 'CredentialsListDefinition', 'InventoryTree',
- 'InventoryStatusDefinition', 'VariablesHelper'])
+ 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'SourceFormDefinition'])
.factory('GetSourceTypeOptions', ['Rest', 'ProcessErrors', 'GetBasePath',
function (Rest, ProcessErrors, GetBasePath) {
@@ -197,14 +197,14 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
if (scope.source.value === 'rax') {
scope.source_region_choices = scope.rax_regions;
//$('#s2id_group_source_regions').select2('data', []);
- $('#s2id_group_source_regions').select2('data', [{
+ $('#s2id_source_source_regions').select2('data', [{
id: 'all',
text: 'All'
}]);
} else if (scope.source.value === 'ec2') {
scope.source_region_choices = scope.ec2_regions;
//$('#s2id_group_source_regions').select2('data', []);
- $('#s2id_group_source_regions').select2('data', [{
+ $('#s2id_source_source_regions').select2('data', [{
id: 'all',
text: 'All'
}]);
@@ -224,7 +224,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
callback = function(){ Wait('stop'); };
Wait('start');
ParseTypeChange({ scope: scope, variable: 'source_vars', parse_variable: form.fields.source_vars.parseTypeName,
- field_id: 'group_source_vars', onReady: callback });
+ field_id: 'source_source_vars', onReady: callback });
}
}
}
@@ -530,15 +530,182 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
}
])
+/**
+ *
+ * Add the list of schedules to the Group Edit modal
+ *
+ */
+.factory('ScheduleList', ['ScheduleEdit', 'SchedulesList', 'GenerateList', 'SearchInit', 'PaginateInit', 'Rest', 'PageRangeSetup',
+'Wait', 'ProcessErrors', 'Find',
+function(ScheduleEdit, SchedulesList, GenerateList, SearchInit, PaginateInit, Rest, PageRangeSetup, Wait, ProcessErrors, Find) {
+ return function(params) {
+ var parent_scope = params.scope,
+ scope, url, list;
+
+ // Clean up
+ $('#schedules-list').hide().empty();
+ $('#schedules-form').hide().empty();
+ $('.tooltip').each(function () {
+ $(this).remove();
+ });
+ $('.popover').each(function () {
+ $(this).remove();
+ });
+
+ // Add schedules list
+ list = angular.copy(SchedulesList);
+ delete list.fields.dtend;
+ delete list.actions.stream;
+ scope = GenerateList.inject(list, {
+ mode: 'edit',
+ id: 'schedules-list',
+ breadCrumbs: false,
+ searchSize: 'col-lg-5 col-md-5 col-sm-6 col-xs-6'
+ });
+
+ $('#schedules-list').show();
+
+ // Change later to use GetBasePath(base)
+ url = '/static/sample/data/schedules/inventory/data.json';
+ SearchInit({
+ scope: scope,
+ set: 'schedules',
+ list: SchedulesList,
+ url: url
+ });
+ PaginateInit({
+ scope: scope,
+ list: SchedulesList,
+ url: url
+ });
+ Rest.setUrl(url);
+ Rest.get()
+ .success(function(data) {
+ var i, modifier;
+ PageRangeSetup({
+ scope: scope,
+ count: data.count,
+ next: data.next,
+ previous: data.previous,
+ iterator: SchedulesList.iterator
+ });
+ scope[SchedulesList.iterator + 'Loading'] = false;
+ for (i = 1; i <= 3; i++) {
+ modifier = (i === 1) ? '' : i;
+ scope[SchedulesList.iterator + 'HoldInput' + modifier] = false;
+ }
+ scope.schedules = data.results;
+ window.scrollTo(0, 0);
+ Wait('stop');
+ scope.$emit('PostRefresh');
+ scope.schedules = data.results;
+ })
+ .error(function(data, status) {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+ msg: 'Call to ' + url + ' failed. GET returned: ' + status });
+ });
+
+ scope.editSchedule = function(id) {
+ var schedule = Find({ list: scope[SchedulesList.name], key: 'id', val: id });
+ ScheduleEdit({ scope: parent_scope, schedule: schedule });
+ };
+ };
+}])
+
+
+/**
+ *
+ * Remove the schedule list, add the schedule widget and populate it with an rrule
+ *
+ */
+.factory('ScheduleEdit', ['SchedulerInit', 'Rest', 'Wait',
+function(SchedulerInit, Rest, Wait) {
+ return function(params) {
+ var parent_scope = params.scope,
+ schedule = params.schedule,
+ scope = parent_scope.$new(),
+ scheduler,
+ target,
+ callback,
+ restore;
+
+ Wait('start');
+ target = $('#schedules-form');
+
+ // Clean up any lingering stuff
+ target.empty().hide();
+ $('.tooltip').each(function () {
+ $(this).remove();
+ });
+ $('.popover').each(function () {
+ $(this).remove();
+ });
+
+ // Insert the scheduler widget into the hidden div
+ scheduler = SchedulerInit({ scope: scope });
+ scheduler.inject('schedules-form', true);
+ scope.showRRuleDetail = false;
+
+ // display the scheduler widget
+ callback = function() {
+ Wait('stop');
+ target.show('slide', { direction: 'left' }, 500);
+ $('#group-save-button').prop('disabled', true);
+ scope.$apply(function() {
+ scheduler.setRRule(schedule.rrule);
+ scheduler.setName(schedule.name);
+ });
+ };
+ $('#schedules-list').hide({ complete: callback, duration: 300 });
+
+ restore = function() {
+ $('#group-save-button').prop('disabled', false);
+ $('#schedules-list').show('slide', { direction: 'right' }, 500);
+ //refresh the list
+ };
+
+ scope.saveForm = function() {
+ var newSchedule,
+ url = '/static/sample/data/schedules/inventory/data.json';
+ if (scheduler.isValid()) {
+ scope.schedulerIsValid = true;
+ Wait('start');
+ newSchedule = scheduler.getValue();
+ schedule.name = newSchedule.name;
+ schedule.rrule = newSchedule.rrule;
+ Rest.setUrl(url);
+ Rest.post(schedule)
+ .success(function(){
+ Wait('stop');
+ target.hide('slide', { direction: 'right' }, 500, restore);
+ })
+ .error(function(){
+ Wait('stop');
+ target.hide('slide', { direction: 'right' }, 500, restore);
+ });
+ }
+ else {
+ scope.schedulerIsValid = false;
+ }
+ };
+
+ scope.resetForm = function() {
+ scheduler.setRRule(schedule.rrule);
+ scheduler.setName(schedule.name);
+ };
+ };
+}])
+
+
+
.factory('GroupsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm',
'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate',
'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find','WatchInventoryWindowResize',
- 'ParseVariableString', 'ToJSON',
+ 'ParseVariableString', 'ToJSON', 'ScheduleList', 'SourceForm',
function ($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors,
GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, LookUpInit, Empty, Wait,
- GetChoices, UpdateGroup, SourceChange, Find, WatchInventoryWindowResize,
- ParseVariableString, ToJSON) {
-
+ GetChoices, UpdateGroup, SourceChange, Find, WatchInventoryWindowResize, ParseVariableString, ToJSON, ScheduleList,
+ SourceForm) {
return function (params) {
var parent_scope = params.scope,
@@ -547,42 +714,49 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
inventory_id = params.inventory_id,
groups_reload = params.groups_reload,
generator = GenerateForm,
- form = GroupForm,
defaultUrl = GetBasePath('groups') + group_id + '/',
master = {},
choicesReady,
- scope, html, x, y, ww, wh, maxrows;
+ modal_scope = parent_scope.$new(),
+ properties_scope = parent_scope.$new(),
+ sources_scope = parent_scope.$new(),
+ x, y, ww, wh, maxrows;
+
+ generator.inject(GroupForm, { mode: 'edit', id: 'properties-tab', breadCrumbs: false, related: false, scope: properties_scope });
+ generator.inject(SourceForm, { mode: 'edit', id: 'sources-tab', breadCrumbs: false, related: false, scope: sources_scope });
- html = "\n";
- $('#inventory-modal-container').empty().append(html);
- scope = generator.inject(form, { mode: 'edit', id: 'form-container', breadCrumbs: false, related: false });
//generator.reset();
- GetSourceTypeOptions({ scope: scope, variable: 'source_type_options' });
- scope.source = form.fields.source['default'];
- scope.sourcePathRequired = false;
- scope[form.fields.source_vars.parseTypeName] = 'yaml';
- scope.parseType = 'yaml';
+
+ GetSourceTypeOptions({ scope: sources_scope, variable: 'source_type_options' });
+ sources_scope.source = SourceForm.fields.source['default'];
+ sources_scope.sourcePathRequired = false;
+ sources_scope[SourceForm.fields.source_vars.parseTypeName] = 'yaml';
+ properties_scope.parseType = 'yaml';
function waitStop() { Wait('stop'); }
+ // Attempt to create the largest textarea field that will fit on the window. Minimum
+ // height is 6 rows, so on short windows you will see vertical scrolling
function textareaResize(textareaID) {
- var formHeight = $('#group_form').height(),
- windowHeight = $('#group-modal-dialog').height(),
- current_height, height, rows, row_height, model;
- Wait('start');
- current_height = $('#' + textareaID).height();
- row_height = Math.floor( current_height / $('#' + textareaID).attr('rows'));
- height = current_height + windowHeight - formHeight;
- rows = Math.floor(height / row_height) - 3;
- rows = (rows < 6) ? 6 : rows;
- $('#' + textareaID).attr('rows', rows);
- if (scope.codeMirror) {
- model = $('#' + textareaID).attr('ng-model');
- scope[model] = scope.codeMirror.getValue();
- scope.codeMirror.destroy();
+ var textArea, formHeight, model, windowHeight, offset, rows;
+ textArea = $('#' + textareaID);
+ if (properties_scope.codeMirror) {
+ model = textArea.attr('ng-model');
+ properties_scope[model] = properties_scope.codeMirror.getValue();
+ properties_scope.codeMirror.destroy();
}
- ParseTypeChange({ scope: scope, field_id: textareaID, onReady: waitStop });
+ textArea.attr('rows', 1);
+ formHeight = $('#group_form').height();
+ windowHeight = $('#group-modal-dialog').height() - 20; //leave a margin of 20px
+ offset = Math.floor(windowHeight - formHeight);
+ rows = Math.floor(offset / 24);
+ rows = (rows < 6) ? 6 : rows;
+ textArea.attr('rows', rows);
+ while(rows > 6 && $('#group_form').height() > $('#group-modal-dialog').height()) {
+ rows--;
+ textArea.attr('rows', rows);
+ }
+ ParseTypeChange({ scope: properties_scope, field_id: textareaID, onReady: waitStop });
}
// Set modal dimensions based on viewport width
@@ -607,20 +781,18 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
$('#group-modal-dialog').dialog({
buttons: {
'Cancel': function() {
- scope.cancelModal();
+ modal_scope.cancelModal();
},
'Save': function () {
- //setTimeout(function(){
- // scope.$apply(function(){
- scope.saveGroup();
- // });
- //});
+ modal_scope.saveGroup();
}
},
modal: true,
width: x,
height: y,
autoOpen: false,
+ minWidth: 440,
+ title: 'Edit Group',
create: function () {
$('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x');
$('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-buttonset button').each(function () {
@@ -651,7 +823,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
content = dialog.find('#group-modal-dialog');
content.width(dialog.width() - 28);
if ($('#group_tabs .active a').text() === 'Properties') {
- textareaResize('group_variables');
+ textareaResize('group_variables', properties_scope);
}
},
close: function () {
@@ -665,131 +837,121 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
$(this).remove();
});
$('#group-modal-dialog').dialog('destroy');
- $('#inventory-modal-container').empty();
- scope.cancelModal();
+ $('#group-modal-dialog').hide();
+ modal_scope.cancelModal();
},
open: function () {
+ $('#group_name').focus();
Wait('stop');
}
});
$('#group_tabs a[data-toggle="tab"]').on('show.bs.tab', function (e) {
- var callback = function(){
- Wait('stop');
- };
if ($(e.target).text() === 'Properties') {
Wait('start');
setTimeout(function(){ textareaResize('group_variables'); }, 300);
- //ParseTypeChange({ scope: scope, field_id: 'group_variables', onReady: callback });
}
- else if ($(e.target).text() === 'Scope') {
- if (scope.source && scope.source.value === 'ec2') {
+ else if ($(e.target).text() === 'Source') {
+ if (sources_scope.source && sources_scope.source.value === 'ec2') {
Wait('start');
- ParseTypeChange({ scope: scope, variable: 'source_vars', parse_variable: form.fields.source_vars.parseTypeName,
- field_id: 'group_source_vars', onReady: callback });
+ ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName,
+ field_id: 'source_source_vars', onReady: waitStop });
}
}
+ else if ($(e.target).text() === 'Schedule') {
+ ScheduleList({ scope: modal_scope });
+ }
});
- if (scope.groupVariablesLoadedRemove) {
- scope.groupVariablesLoadedRemove();
+ if (modal_scope.groupVariablesLoadedRemove) {
+ modal_scope.groupVariablesLoadedRemove();
}
- scope.groupVariablesLoadedRemove = scope.$on('groupVariablesLoaded', function () {
- //$('#group_tabs a:first').tab('show');
-
- //ParseTypeChange({ scope: scope, field_id: 'group_variables', onReady: callback });
+ modal_scope.groupVariablesLoadedRemove = modal_scope.$on('groupVariablesLoaded', function () {
+ $('#group_tabs a:first').tab('show');
Wait('start');
$('#group-modal-dialog').dialog('open');
- setTimeout(function() { textareaResize('group_variables'); }, 300);
+ setTimeout(function() { textareaResize('group_variables', properties_scope); }, 300);
});
// After the group record is loaded, retrieve related data
- if (scope.groupLoadedRemove) {
- scope.groupLoadedRemove();
+ if (modal_scope.groupLoadedRemove) {
+ modal_scope.groupLoadedRemove();
}
- scope.groupLoadedRemove = scope.$on('groupLoaded', function () {
- if (scope.variable_url) {
+ modal_scope.groupLoadedRemove = modal_scope.$on('groupLoaded', function () {
+ if (properties_scope.variable_url) {
// get group variables
- Rest.setUrl(scope.variable_url);
+ Rest.setUrl(properties_scope.variable_url);
Rest.get()
.success(function (data) {
- scope.variables = ParseVariableString(data);
- master.variables = scope.variables;
- scope.$emit('groupVariablesLoaded');
+ properties_scope.variables = ParseVariableString(data);
+ master.variables = properties_scope.variables;
+ modal_scope.$emit('groupVariablesLoaded');
})
.error(function (data, status) {
- scope.variables = null;
- ProcessErrors(scope, data, status, form, { hdr: 'Error!',
+ properties_scope.variables = null;
+ ProcessErrors(modal_scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve group variables. GET returned status: ' + status });
});
} else {
- scope.variables = "---";
- master.variables = scope.variables;
- scope.$emit('groupVariablesLoaded');
+ properties_scope.variables = "---";
+ master.variables = properties_scope.variables;
+ properties_scope.$emit('groupVariablesLoaded');
}
- if (scope.source_url) {
+ if (sources_scope.source_url) {
// get source data
- Rest.setUrl(scope.source_url);
+ Rest.setUrl(sources_scope.source_url);
Rest.get()
.success(function (data) {
- var fld, i, j, flag, found, set, opts, list;
+ var fld, i, j, flag, found, set, opts, list, form;
+ form = SourceForm;
for (fld in form.fields) {
if (fld === 'checkbox_group') {
for (i = 0; i < form.fields[fld].fields.length; i++) {
flag = form.fields[fld].fields[i];
if (data[flag.name] !== undefined) {
- scope[flag.name] = data[flag.name];
- master[flag.name] = scope[flag.name];
+ sources_scope[flag.name] = data[flag.name];
+ master[flag.name] = sources_scope[flag.name];
}
}
}
if (fld === 'source') {
found = false;
- for (i = 0; i < scope.source_type_options.length; i++) {
- if (scope.source_type_options[i].value === data.source) {
- scope.source = scope.source_type_options[i];
+ for (i = 0; i < sources_scope.source_type_options.length; i++) {
+ if (sources_scope.source_type_options[i].value === data.source) {
+ sources_scope.source = sources_scope.source_type_options[i];
found = true;
}
}
- if (!found || scope.source.value === "") {
- scope.groupUpdateHide = true;
+ if (!found || sources_scope.source.value === "") {
+ sources_scope.groupUpdateHide = true;
} else {
- scope.groupUpdateHide = false;
- }
- master.source = scope.source;
- } else if (fld === 'update_interval') {
- if (data[fld] === '' || data[fld] === null || data[fld] === undefined) {
- data[fld] = 0;
- }
- for (i = 0; i < scope.update_interval_options.length; i++) {
- if (scope.update_interval_options[i].value === data[fld]) {
- scope[fld] = scope.update_interval_options[i];
- }
+ sources_scope.groupUpdateHide = false;
}
+ master.source = sources_scope.source;
} else if (fld === 'source_vars') {
// Parse source_vars, converting to YAML.
- scope.source_vars = ParseVariableString(data.source_vars);
- master.source_vars = scope.variables;
+ sources_scope.source_vars = ParseVariableString(data.source_vars);
+ master.source_vars = sources_scope.variables;
} else if (data[fld]) {
- scope[fld] = data[fld];
- master[fld] = scope[fld];
+ sources_scope[fld] = data[fld];
+ master[fld] = sources_scope[fld];
}
if (form.fields[fld].sourceModel && data.summary_fields &&
data.summary_fields[form.fields[fld].sourceModel]) {
- scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
+ sources_scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
- scope.sourceChange(); //set defaults that rely on source value
+ sources_scope.sourceChange(); //set defaults that rely on source value
if (data.source_regions) {
if (data.source === 'ec2' || data.source === 'rax') {
- set = (data.source === 'ec2') ? scope.ec2_regions : scope.rax_regions;
+ set = (data.source === 'ec2') ? sources_scope.ec2_regions : sources_scope.rax_regions;
opts = [];
list = data.source_regions.split(',');
for (i = 0; i < list.length; i++) {
@@ -803,7 +965,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
}
}
master.source_regions = opts;
- $('#s2id_group_source_regions').select2('data', opts);
+ $('#s2id_source_source_regions').select2('data', opts);
}
} else {
// If empty, default to all
@@ -811,58 +973,58 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
id: 'all',
text: 'All'
}];
+ $('#s2id_source_source_regions').select2('data', master.source_regions);
}
- scope.group_update_url = data.related.update;
+ sources_scope.group_update_url = data.related.update;
//Wait('stop');
})
.error(function (data, status) {
- scope.source = "";
- ProcessErrors(scope, data, status, form, { hdr: 'Error!',
+ sources_scope.source = "";
+ ProcessErrors(modal_scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve inventory source. GET status: ' + status });
});
}
});
- if (scope.removeChoicesComplete) {
- scope.removeChoicesComplete();
+ if (modal_scope.removeChoicesComplete) {
+ modal_scope.removeChoicesComplete();
}
- scope.removeChoicesComplete = scope.$on('choicesCompleteGroup', function () {
+ modal_scope.removeChoicesComplete = modal_scope.$on('choicesCompleteGroup', function () {
// Retrieve detail record and prepopulate the form
Rest.setUrl(defaultUrl);
Rest.get()
.success(function (data) {
- for (var fld in form.fields) {
+ for (var fld in GroupForm.fields) {
if (data[fld]) {
- scope[fld] = data[fld];
- master[fld] = scope[fld];
+ properties_scope[fld] = data[fld];
+ master[fld] = properties_scope[fld];
}
}
- scope.variable_url = data.related.variable_data;
- scope.source_url = data.related.inventory_source;
- //$('#form-modal').modal('show');
- scope.$emit('groupLoaded');
+ properties_scope.variable_url = data.related.variable_data;
+ sources_scope.source_url = data.related.inventory_source;
+ modal_scope.$emit('groupLoaded');
})
.error(function (data, status) {
- ProcessErrors(scope, data, status, form, { hdr: 'Error!',
+ ProcessErrors(modal_scope, data, status, { hdr: 'Error!',
msg: 'Failed to retrieve group: ' + defaultUrl + '. GET status: ' + status });
});
});
choicesReady = 0;
- if (scope.removeChoicesReady) {
- scope.removeChoicesReady();
+ if (sources_scope.removeChoicesReady) {
+ sources_scope.removeChoicesReady();
}
- scope.removeChoicesReady = scope.$on('choicesReadyGroup', function () {
+ sources_scope.removeChoicesReady = sources_scope.$on('choicesReadyGroup', function () {
choicesReady++;
if (choicesReady === 2) {
- scope.$emit('choicesCompleteGroup');
+ modal_scope.$emit('choicesCompleteGroup');
}
});
// Load options for source regions
GetChoices({
- scope: scope,
+ scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'rax_regions',
@@ -871,7 +1033,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
});
GetChoices({
- scope: scope,
+ scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'ec2_regions',
@@ -881,10 +1043,10 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
Wait('start');
- if (scope.removeSaveComplete) {
- scope.removeSaveComplete();
+ if (modal_scope.removeSaveComplete) {
+ modal_scope.removeSaveComplete();
}
- scope.removeSaveComplete = scope.$on('SaveComplete', function (e, error) {
+ modal_scope.removeSaveComplete = modal_scope.$on('SaveComplete', function (e, error) {
if (!error) {
// Update the view with any changes
if (groups_reload) {
@@ -892,13 +1054,13 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
scope: parent_scope,
group_id: group_id,
properties: {
- name: scope.name,
- description: scope.description,
- has_inventory_sources: (scope.source && scope.source.value) ? true : false,
- source: (scope.source && scope.source.value) ? scope.source.value : ''
+ name: properties_scope.name,
+ description: properties_scope.description,
+ has_inventory_sources: (sources_scope.source && sources_scope.source.value) ? true : false,
+ source: (sources_scope.source && sources_scope.source.value) ? sources_scope.source.value : ''
}
});
- } else if (scope.home_groups) {
+ } else if (parent_scope.home_groups) {
// When home.groups controller is calling, update the groups array
var g = Find({
list: parent_scope.home_groups,
@@ -906,18 +1068,16 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
val: group_id
});
if (g) {
- g.name = scope.name;
- g.description = scope.description;
+ g.name = properties_scope.name;
+ g.description = properties_scope.description;
}
}
//Clean up
- if (scope.searchCleanUp) {
- scope.searchCleanup();
+ if (modal_scope.searchCleanUp) {
+ modal_scope.searchCleanup();
}
- scope.formModalActionDisabled = false;
-
$('#group-modal-dialog').dialog('close');
// Change the selected group
@@ -930,10 +1090,10 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
}
});
- if (scope.removeFormSaveSuccess) {
- scope.removeFormSaveSuccess();
+ if (modal_scope.removeFormSaveSuccess) {
+ modal_scope.removeFormSaveSuccess();
}
- scope.removeFormSaveSuccess = scope.$on('formSaveSuccess', function () {
+ modal_scope.removeFormSaveSuccess = modal_scope.$on('formSaveSuccess', function () {
// Source data gets stored separately from the group. Validate and store Source
// related fields, then call SaveComplete to wrap things up.
@@ -942,65 +1102,67 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
regions, r, i,
data = {
group: group_id,
- source: ((scope.source && scope.source.value) ? scope.source.value : ''),
- source_path: scope.source_path,
- credential: scope.credential,
- overwrite: scope.overwrite,
- overwrite_vars: scope.overwrite_vars,
- update_on_launch: scope.update_on_launch
+ source: ((sources_scope.source && sources_scope.source.value) ? sources_scope.source.value : ''),
+ source_path: sources_scope.source_path,
+ credential: sources_scope.credential,
+ overwrite: sources_scope.overwrite,
+ overwrite_vars: sources_scope.overwrite_vars,
+ update_on_launch: sources_scope.update_on_launch
};
// Create a string out of selected list of regions
- regions = $('#s2id_group_source_regions').select2("data");
+ regions = $('#s2id_source_source_regions').select2("data");
r = [];
for (i = 0; i < regions.length; i++) {
r.push(regions[i].id);
}
data.source_regions = r.join();
- if (scope.source && scope.source.value === 'ec2') {
+ if (sources_scope.source && sources_scope.source.value === 'ec2') {
// for ec2, validate variable data
- data.source_vars = ToJSON(scope.envParseType, scope.source_vars, true);
+ data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true);
}
if (!parseError) {
- Rest.setUrl(scope.source_url);
+ Rest.setUrl(sources_scope.source_url);
Rest.put(data)
.success(function () {
- scope.$emit('SaveComplete', false);
+ modal_scope.$emit('SaveComplete', false);
})
.error(function (data, status) {
- scope.$emit('SaveComplete', true);
- ProcessErrors(scope, data, status, form, { hdr: 'Error!',
+ modal_scope.$emit('SaveComplete', true);
+ ProcessErrors(sources_scope, data, status, SourceForm, { hdr: 'Error!',
msg: 'Failed to update group inventory source. PUT status: ' + status });
});
}
});
-
+
// Cancel
- scope.cancelModal = function () {
+ modal_scope.cancelModal = function () {
try {
$('#group-modal-dialog').dialog('close');
}
catch(e) {
//ignore
}
- if (scope.searchCleanup) {
- scope.searchCleanup();
+ if (modal_scope.searchCleanup) {
+ modal_scope.searchCleanup();
}
WatchInventoryWindowResize();
};
// Save
- scope.saveGroup = function () {
+ modal_scope.saveGroup = function () {
Wait('start');
var fld, data, json_data;
- json_data = ToJSON(scope.parseType, scope.variables);
+ json_data = ToJSON(properties_scope.parseType, properties_scope.variables);
data = {};
- for (fld in form.fields) {
- data[fld] = scope[fld];
+ for (fld in GroupForm.fields) {
+ if (fld !== 'variables') {
+ data[fld] = properties_scope[fld];
+ }
}
data.inventory = inventory_id;
@@ -1008,52 +1170,51 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
Rest.setUrl(defaultUrl);
Rest.put(data)
.success(function () {
- if (scope.variables) {
+ if (properties_scope.variables) {
//update group variables
- Rest.setUrl(scope.variable_url);
+ Rest.setUrl(properties_scope.variable_url);
Rest.put(json_data)
.success(function () {
- scope.$emit('formSaveSuccess');
+ modal_scope.$emit('formSaveSuccess');
})
.error(function (data, status) {
- ProcessErrors(scope, data, status, form, { hdr: 'Error!',
+ ProcessErrors(modal_scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to update group variables. PUT status: ' + status });
});
} else {
- scope.$emit('formSaveSuccess');
+ modal_scope.$emit('formSaveSuccess');
}
})
.error(function (data, status) {
Wait('stop');
- ProcessErrors(scope, data, status, form, {
- hdr: 'Error!',
+ ProcessErrors(properties_scope, data, status, GroupForm, { hdr: 'Error!',
msg: 'Failed to update group: ' + group_id + '. PUT status: ' + status
});
});
};
// Start the update process
- scope.updateGroup = function () {
- if (scope.source === "" || scope.source === null) {
+ modal_scope.updateGroup = function () {
+ if (sources_scope.source === "" || sources_scope.source === null) {
Alert('Missing Configuration', 'The selected group is not configured for updates. You must first edit the group, provide Source settings, ' +
'and then run an update.', 'alert-info');
- } else if (scope.status === 'updating') {
+ } else if (sources_scope.status === 'updating') {
Alert('Update in Progress', 'The inventory update process is currently running for group ' +
- scope.summary_fields.group.name + '. Use the Refresh button to monitor the status.', 'alert-info');
+ sources_scope.summary_fields.group.name + '. Use the Refresh button to monitor the status.', 'alert-info');
} else {
InventoryUpdate({
- scope: scope,
+ scope: parent_scope,
group_id: group_id,
- url: scope.group_update_url,
- group_name: scope.name,
- group_source: scope.source.value
+ url: properties_scope.group_update_url,
+ group_name: properties_scope.name,
+ group_source: sources_scope.source.value
});
}
};
// Change the lookup and regions when the source changes
- scope.sourceChange = function () {
- SourceChange({ scope: scope, form: GroupForm });
+ sources_scope.sourceChange = function () {
+ SourceChange({ scope: sources_scope, form: SourceForm });
};
};
diff --git a/awx/ui/static/js/lists/Schedules.js b/awx/ui/static/js/lists/Schedules.js
index ff01670cbb..2e90ea675e 100644
--- a/awx/ui/static/js/lists/Schedules.js
+++ b/awx/ui/static/js/lists/Schedules.js
@@ -15,6 +15,7 @@ angular.module('SchedulesListDefinition', [])
iterator: 'schedule',
selectTitle: '',
editTitle: 'Schedules',
+ well: false,
index: true,
hover: true,
diff --git a/awx/ui/static/less/angular-scheduler.less b/awx/ui/static/less/angular-scheduler.less
index d81ff45e3b..d03d3f56b8 100644
--- a/awx/ui/static/less/angular-scheduler.less
+++ b/awx/ui/static/less/angular-scheduler.less
@@ -5,7 +5,18 @@
*
*/
-#scheduler-modal-dialog {
+ /*
+ #schedules-form -inventory group add/edit dialog
+ */
+
+#schedules-form {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ box-shadow: 3px 3px 6px 0 #666;
+ padding: 8px 10px 15px 8px;
+}
+
+#scheduler-modal-dialog, #schedules-form {
display: none;
overflow-x: hidden;
overflow-y: auto;
@@ -83,6 +94,13 @@
margin-top: 0;
padding-top: 3px;
}
+
+ .error-pull-up {
+ position: relative;
+ top: -15px;
+ margin-bottom: 15px;
+ }
+
.red-text {
color: #dd1b16;
}
diff --git a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.js b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.js
index 8e4b92e8be..42da67847e 100644
--- a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.js
+++ b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.js
@@ -130,9 +130,11 @@ angular.module('AngularScheduler', ['underscore'])
scope.startDateError = function(msg) {
if (scope.scheduler_form) {
- scope.scheduler_form_schedulerStartDt_error = msg;
- scope.scheduler_form.schedulerStartDt.$pristine = false;
- scope.scheduler_form.schedulerStartDt.$dirty = true;
+ if (scope.scheduler_form.schedulerStartDt) {
+ scope.scheduler_form_schedulerStartDt_error = msg;
+ scope.scheduler_form.schedulerStartDt.$pristine = false;
+ scope.scheduler_form.schedulerStartDt.$dirty = true;
+ }
$('#schedulerStartDt').removeClass('ng-pristine').removeClass('ng-valid').removeClass('ng-valid-custom-error')
.addClass('ng-dirty').addClass('ng-invalid').addClass('ng-invalid-custom-error');
}
@@ -141,8 +143,10 @@ angular.module('AngularScheduler', ['underscore'])
scope.resetStartDate = function() {
if (scope.scheduler_form) {
scope.scheduler_form_schedulerStartDt_error = '';
- scope.scheduler_form.schedulerStartDt.$setValidity('custom-error', true);
- scope.scheduler_form.schedulerStartDt.$setPristine();
+ if (scope.scheduler_form.schedulerStartDt) {
+ scope.scheduler_form.schedulerStartDt.$setValidity('custom-error', true);
+ scope.scheduler_form.schedulerStartDt.$setPristine();
+ }
}
};
@@ -225,9 +229,11 @@ angular.module('AngularScheduler', ['underscore'])
this.scope.scheduler_endDt_error = false;
this.scope.resetStartDate();
this.scope.scheduler_endDt_error = false;
- this.scope.scheduler_form.schedulerEndDt.$setValidity('custom-error', true);
- this.scope.scheduler_form.schedulerEndDt.$setPristine();
- this.scope.scheduler_form.$setPristine();
+ if (this.scope.scheduler_form && this.scope.scheduler_form.schedulerEndDt) {
+ this.scope.scheduler_form.schedulerEndDt.$setValidity('custom-error', true);
+ this.scope.scheduler_form.schedulerEndDt.$setPristine();
+ this.scope.scheduler_form.$setPristine();
+ }
};
// Check the input form for errors
@@ -333,7 +339,9 @@ angular.module('AngularScheduler', ['underscore'])
// Clear the form, returning all elements to a default state
this.clear = function() {
this.clearErrors();
- this.scope.scheduler_form.schedulerName.$setPristine();
+ if (this.scope.scheduler_form && this.scope.scheduler_form.schedulerName) {
+ this.scope.scheduler_form.schedulerName.$setPristine();
+ }
this.scope.setDefaults();
};
diff --git a/awx/ui/static/partials/inventory-edit.html b/awx/ui/static/partials/inventory-edit.html
index 7d8a31c63b..8137991f11 100644
--- a/awx/ui/static/partials/inventory-edit.html
+++ b/awx/ui/static/partials/inventory-edit.html
@@ -10,6 +10,27 @@
+
+
+
+
\ No newline at end of file
diff --git a/awx/ui/static/sample/data/schedules/inventory/data.json b/awx/ui/static/sample/data/schedules/inventory/data.json
new file mode 100644
index 0000000000..e377db960f
--- /dev/null
+++ b/awx/ui/static/sample/data/schedules/inventory/data.json
@@ -0,0 +1,43 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "job_template": null,
+ "inventory_source": 8,
+ "project": null,
+ "job_class": "inventory:sync",
+ "job_type": "inventory_sync",
+ "name": "Hourly",
+ "dtstart": "2014-03-10T17:00:00.000Z" ,
+ "dtend": null,
+ "rrule": "FREQ=HOURLY;DTSTART=20140310T170000Z;INTERVAL=1"
+ },
+ {
+ "id": 2,
+ "job_template": null,
+ "inventory_source": 8,
+ "project": null,
+ "job_class": "inventory:sync",
+ "job_type": "inventory_sync",
+ "name": "Weekly",
+ "dtstart": "2014-03-17T13:00:00.000Z",
+ "dtend": null,
+ "rrule": "FREQ=WEEKLY;DTSTART=20140317T130000Z;INTERVAL=1;COUNT=10;BYDAY=MO"
+ },
+ {
+ "id": 3,
+ "job_template": null,
+ "inventory_source": 8,
+ "project": null,
+ "job_class": "inventory:sync",
+ "job_type": "inventory_sync",
+ "name": "Monthly",
+ "dtstart": "2014-04-06T01:00:00.000Z",
+ "dtend": "2020-03-01T01:00:00.000Z",
+ "rrule": "FREQ=MONTHLY;DTSTART=20140406T010000Z;INTERVAL=1;UNTIL=20200301T010000Z;BYMONTHDAY=1"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html
index 73301cab58..8dd4354096 100644
--- a/awx/ui/templates/ui/index.html
+++ b/awx/ui/templates/ui/index.html
@@ -96,6 +96,7 @@
+