diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js
index d3a09f6013..e8916ec304 100644
--- a/awx/ui/static/js/app.js
+++ b/awx/ui/static/js/app.js
@@ -86,8 +86,18 @@ angular.module('ansible', [
'HomeGroupListDefinition',
'HomeHostListDefinition',
'ActivityDetailDefinition',
- 'VariablesHelper'
+ 'VariablesHelper',
+ 'SchedulesListDefinition',
+ 'AngularScheduler',
+ 'Timezones',
+ 'SchedulesHelper'
])
+
+ .constant('AngularScheduler.partial', $basePath + 'lib/angular-scheduler/lib/angular-scheduler.html')
+ .constant('AngularScheduler.useTimezone', false)
+ .constant('AngularScheduler.showUTCField', false)
+ .constant('$timezones.definitions.location', $basePath + 'lib/angular-tz-extensions/tz/data')
+
.config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
@@ -131,6 +141,11 @@ angular.module('ansible', [
controller: 'JobTemplatesEdit'
}).
+ when('/job_templates/:id/schedules', {
+ templateUrl: urlPrefix + 'partials/schedule_detail.html',
+ controller: 'ScheduleEdit'
+ }).
+
when('/projects', {
templateUrl: urlPrefix + 'partials/projects.html',
controller: 'ProjectsList'
@@ -145,6 +160,11 @@ angular.module('ansible', [
templateUrl: urlPrefix + 'partials/projects.html',
controller: 'ProjectsEdit'
}).
+
+ when('/projects/:id/schedules', {
+ templateUrl: urlPrefix + 'partials/schedule_detail.html',
+ controller: 'ScheduleEdit'
+ }).
when('/projects/:project_id/organizations', {
templateUrl: urlPrefix + 'partials/projects.html',
diff --git a/awx/ui/static/js/controllers/Schedules.js b/awx/ui/static/js/controllers/Schedules.js
new file mode 100644
index 0000000000..0177f46d6b
--- /dev/null
+++ b/awx/ui/static/js/controllers/Schedules.js
@@ -0,0 +1,135 @@
+
+/************************************
+ * Copyright (c) 2014 AnsibleWorks, Inc.
+ *
+ * Schedules.js
+ *
+ * Controller functions for the Schedule model.
+ *
+ */
+
+'use strict';
+
+function ScheduleEdit($scope, $compile, $location, $routeParams, SchedulesList, GenerateList, Rest, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope,
+GetBasePath, LookUpInit, Wait, SchedulerInit, Breadcrumbs, SearchInit, PaginateInit, PageRangeSetup, EditSchedule, AddSchedule, Find) {
+
+ ClearScope();
+
+ var base, e, id, job_template, url;
+
+ base = $location.path().replace(/^\//, '').split('/')[0];
+
+ $scope.$on('job_template_ready', function() {
+ // Add breadcrumbs
+ LoadBreadCrumbs({
+ path: $location.path().replace(/\/schedules$/,''),
+ title: job_template.name
+ });
+ e = angular.element(document.getElementById('breadcrumbs'));
+ e.html(Breadcrumbs({ list: SchedulesList, mode: 'edit' }));
+ $compile(e)($scope);
+
+ // Add schedules list
+ GenerateList.inject(SchedulesList, {
+ mode: 'edit',
+ id: 'schedule-list-target',
+ breadCrumbs: false,
+ searchSize: 'col-lg-4 col-md-4 col-sm-3'
+ });
+
+ // Change later to use GetBasePath(base)
+ switch(base) {
+ case 'job_templates':
+ url = '/static/sample/data/schedules/data.json';
+ break;
+ case 'projects':
+ url = '/static/sample/data/schedules/projects/data.json';
+ break;
+ }
+ 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.schedules, key: 'id', val: id });
+ EditSchedule({ scope: $scope, schedule: schedule, url: url });
+ };
+
+ $scope.addSchedule = function() {
+ var schedule = { };
+ switch(base) {
+ case 'job_templates':
+ schedule.job_template = $routeParams.id;
+ schedule.job_type = 'playbook_run';
+ schedule.job_class = "ansible:playbook";
+ break;
+ case 'inventories':
+ schedule.inventory = $routeParams.id;
+ schedule.job_type = 'inventory_sync';
+ schedule.job_class = "inventory:sync";
+ break;
+ case 'projects':
+ schedule.project = $routeParams.id;
+ schedule.job_type = 'project_sync';
+ schedule.job_class = "project:sync";
+ }
+ AddSchedule({ scope: $scope, schedule: schedule, url: url });
+ };
+
+
+ //scheduler = SchedulerInit({ scope: $scope });
+ //scheduler.inject('scheduler-target', true);
+
+ id = $routeParams.id;
+ Rest.setUrl(GetBasePath(base) + id);
+ Rest.get()
+ .success(function(data) {
+ job_template = data;
+ $scope.$emit('job_template_ready');
+ })
+ .error(function(status) {
+ ProcessErrors($scope, null, status, null, { hdr: 'Error!',
+ msg: 'Call to ' + url + ' failed. GET returned: ' + status });
+ });
+}
+
+ScheduleEdit.$inject = ['$scope', '$compile', '$location', '$routeParams', 'SchedulesList', 'GenerateList', 'Rest', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller',
+'ClearScope', 'GetBasePath', 'LookUpInit', 'Wait', 'SchedulerInit', 'Breadcrumbs', 'SearchInit', 'PaginateInit', 'PageRangeSetup', 'EditSchedule', 'AddSchedule',
+'Find'
+];
\ No newline at end of file
diff --git a/awx/ui/static/js/forms/Groups.js b/awx/ui/static/js/forms/Groups.js
index 8bf627a669..1569abbc10 100644
--- a/awx/ui/static/js/forms/Groups.js
+++ b/awx/ui/static/js/forms/Groups.js
@@ -14,7 +14,7 @@ angular.module('GroupFormDefinition', [])
showTitle: true,
cancelButton: false,
name: 'group',
- well: true,
+ well: false,
formLabelSize: 'col-lg-3',
formFieldSize: 'col-lg-9',
@@ -24,6 +24,9 @@ angular.module('GroupFormDefinition', [])
}, {
name: 'source',
label: 'Source'
+ },{
+ name: 'schedules',
+ label: 'Schedules'
}],
fields: {
@@ -46,7 +49,7 @@ angular.module('GroupFormDefinition', [])
type: 'textarea',
addRequired: false,
editRequird: false,
- rows: 6,
+ rows: 12,
'default': '---',
dataTitle: 'Group Variables',
dataPlacement: 'right',
@@ -184,7 +187,7 @@ angular.module('GroupFormDefinition', [])
},
buttons: {
-
+ /*
labelClass: 'col-lg-3',
controlClass: 'col-lg-5',
@@ -196,6 +199,7 @@ angular.module('GroupFormDefinition', [])
ngClick: 'formReset()',
ngDisabled: true //Disabled when $pristine
}
+ */
},
related: { }
diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js
index 6feb58babe..0ae218142e 100644
--- a/awx/ui/static/js/forms/JobTemplates.js
+++ b/awx/ui/static/js/forms/JobTemplates.js
@@ -346,6 +346,54 @@ angular.module('JobTemplateFormDefinition', [])
icon: 'icon-zoom-in'
}
}
+ },
+
+ schedules: {
+ type: 'collection',
+ title: 'Schedules',
+ iterator: 'schedule',
+ index: true,
+ open: false,
+
+ fields: {
+ name: {
+ key: true,
+ label: 'Name'
+ },
+ dtstart: {
+ label: 'Start'
+ },
+ dtend: {
+ label: 'End'
+ }
+ },
+
+ actions: {
+ add: {
+ mode: 'all',
+ ngClick: 'addSchedule()',
+ awToolTip: 'Add a new schedule'
+ }
+ },
+
+ fieldActions: {
+ edit: {
+ label: 'Edit',
+ ngClick: "editSchedule(schedule.id)",
+ icon: 'icon-edit',
+ awToolTip: 'Edit schedule',
+ dataPlacement: 'top'
+ },
+
+ "delete": {
+ label: 'Delete',
+ ngClick: "deleteSchedule(schedule.id)",
+ icon: 'icon-trash',
+ awToolTip: 'Delete schedule',
+ dataPlacement: 'top'
+ }
+ }
+
}
}
diff --git a/awx/ui/static/js/forms/Projects.js b/awx/ui/static/js/forms/Projects.js
index 1463b2aa88..20fd460a35 100644
--- a/awx/ui/static/js/forms/Projects.js
+++ b/awx/ui/static/js/forms/Projects.js
@@ -129,9 +129,9 @@ angular.module('ProjectFormDefinition', [])
hdr: 'GIT URLs',
content: '
Example URLs for GIT SCM include:
- https://github.com/ansible/ansible.git
' +
'- git@github.com:ansible/ansible.git
- git://servername.example.com/ansible.git
' +
- 'Note: If using SSH protocol for GitHub or Bitbucket, enter in the SSH key only, ' +
+ '
Note: When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' +
- 'SSH protocol. GIT read only protocol (git://) does not use username or password information.',
+ 'SSH. GIT read only protocol (git://) does not use username or password information.',
show: "scm_type.value == 'git'"
}, {
hdr: 'SVN URLs',
@@ -262,6 +262,54 @@ angular.module('ProjectFormDefinition', [])
awToolTip: 'Delete the organization'
}
}
+ },
+
+ schedules: {
+ type: 'collection',
+ title: 'Schedules',
+ iterator: 'schedule',
+ index: true,
+ open: false,
+
+ fields: {
+ name: {
+ key: true,
+ label: 'Name'
+ },
+ dtstart: {
+ label: 'Start'
+ },
+ dtend: {
+ label: 'End'
+ }
+ },
+
+ actions: {
+ add: {
+ mode: 'all',
+ ngClick: 'addSchedule()',
+ awToolTip: 'Add a new schedule'
+ }
+ },
+
+ fieldActions: {
+ edit: {
+ label: 'Edit',
+ ngClick: "editSchedule(schedule.id)",
+ icon: 'icon-edit',
+ awToolTip: 'Edit schedule',
+ dataPlacement: 'top'
+ },
+
+ "delete": {
+ label: 'Delete',
+ ngClick: "deleteSchedule(schedule.id)",
+ icon: 'icon-trash',
+ awToolTip: 'Delete schedule',
+ dataPlacement: 'top'
+ }
+ }
+
}
}
diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js
index 3684935e31..e7d792b32a 100644
--- a/awx/ui/static/js/helpers/Groups.js
+++ b/awx/ui/static/js/helpers/Groups.js
@@ -551,25 +551,138 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
defaultUrl = GetBasePath('groups') + group_id + '/',
master = {},
choicesReady,
- scope = generator.inject(form, { mode: 'edit', modal: true, related: false, show_modal: false });
+ scope, html, x, y, ww, wh, maxrows;
- generator.reset();
-
+ 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.formModalActionLabel = 'Save';
- scope.formModalHeader = 'Group';
- scope.formModalCancelShow = true;
scope.source = form.fields.source['default'];
scope.sourcePathRequired = false;
+ scope[form.fields.source_vars.parseTypeName] = 'yaml';
+ scope.parseType = 'yaml';
+
+ function waitStop() { Wait('stop'); }
+
+ 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();
+ }
+ ParseTypeChange({ scope: scope, field_id: textareaID, onReady: waitStop });
+ }
+
+ // Set modal dimensions based on viewport width
+ ww = $(document).width();
+ wh = $('body').height();
+ if (ww > 1199) {
+ // desktop
+ x = 675;
+ y = (750 > wh) ? wh - 20 : 750;
+ maxrows = 18;
+ } else if (ww <= 1199 && ww >= 768) {
+ x = 550;
+ y = (620 > wh) ? wh - 15 : 620;
+ maxrows = 12;
+ } else {
+ x = (ww - 20);
+ y = (500 > wh) ? wh : 500;
+ maxrows = 10;
+ }
+
+ // Create the modal
+ $('#group-modal-dialog').dialog({
+ buttons: {
+ 'Cancel': function() {
+ scope.cancelModal();
+ },
+ 'Save': function () {
+ //setTimeout(function(){
+ // scope.$apply(function(){
+ scope.saveGroup();
+ // });
+ //});
+ }
+ },
+ modal: true,
+ width: x,
+ height: y,
+ autoOpen: false,
+ 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 () {
+ var c, h, i, l;
+ l = $(this).text();
+ if (l === 'Cancel') {
+ h = "fa-times";
+ c = "btn btn-default";
+ i = "group-close-button";
+ $(this).attr({
+ 'class': c,
+ 'id': i
+ }).html(" Cancel");
+ } else if (l === 'Save') {
+ h = "fa-check";
+ c = "btn btn-primary";
+ i = "group-save-button";
+ $(this).attr({
+ 'class': c,
+ 'id': i
+ }).html(" Save");
+ }
+ });
+ },
+ resizeStop: function () {
+ // for some reason, after resizing dialog the form and fields (the content) doesn't expand to 100%
+ var dialog = $('.ui-dialog[aria-describedby="group-modal-dialog"]'),
+ content = dialog.find('#group-modal-dialog');
+ content.width(dialog.width() - 28);
+ if ($('#group_tabs .active a').text() === 'Properties') {
+ textareaResize('group_variables');
+ }
+ },
+ close: function () {
+ // Destroy on close
+ $('.tooltip').each(function () {
+ // Remove any lingering tooltip elements
+ $(this).remove();
+ });
+ $('.popover').each(function () {
+ // remove lingering popover
elements
+ $(this).remove();
+ });
+ $('#group-modal-dialog').dialog('destroy');
+ $('#inventory-modal-container').empty();
+ scope.cancelModal();
+ },
+ open: function () {
+ Wait('stop');
+ }
+ });
$('#group_tabs a[data-toggle="tab"]').on('show.bs.tab', function (e) {
- var callback = function(){ Wait('stop'); };
+ var callback = function(){
+ Wait('stop');
+ };
if ($(e.target).text() === 'Properties') {
Wait('start');
- ParseTypeChange({ scope: scope, field_id: 'group_variables', onReady: callback });
+ setTimeout(function(){ textareaResize('group_variables'); }, 300);
+ //ParseTypeChange({ scope: scope, field_id: 'group_variables', onReady: callback });
}
- else {
+ else if ($(e.target).text() === 'Scope') {
if (scope.source && scope.source.value === 'ec2') {
Wait('start');
ParseTypeChange({ scope: scope, variable: 'source_vars', parse_variable: form.fields.source_vars.parseTypeName,
@@ -578,15 +691,16 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
}
});
- scope[form.fields.source_vars.parseTypeName] = 'yaml';
- scope.parseType = 'yaml';
-
if (scope.groupVariablesLoadedRemove) {
scope.groupVariablesLoadedRemove();
}
scope.groupVariablesLoadedRemove = scope.$on('groupVariablesLoaded', function () {
- var callback = function() { Wait('stop'); };
- ParseTypeChange({ scope: scope, field_id: 'group_variables', onReady: callback });
+ //$('#group_tabs a:first').tab('show');
+
+ //ParseTypeChange({ scope: scope, field_id: 'group_variables', onReady: callback });
+ Wait('start');
+ $('#group-modal-dialog').dialog('open');
+ setTimeout(function() { textareaResize('group_variables'); }, 300);
});
// After the group record is loaded, retrieve related data
@@ -600,6 +714,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
Rest.get()
.success(function (data) {
scope.variables = ParseVariableString(data);
+ master.variables = scope.variables;
scope.$emit('groupVariablesLoaded');
})
.error(function (data, status) {
@@ -609,10 +724,10 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
});
} else {
scope.variables = "---";
+ master.variables = scope.variables;
scope.$emit('groupVariablesLoaded');
}
- master.variables = scope.variables;
-
+
if (scope.source_url) {
// get source data
Rest.setUrl(scope.source_url);
@@ -724,7 +839,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
}
scope.variable_url = data.related.variable_data;
scope.source_url = data.related.inventory_source;
- $('#form-modal').modal('show');
+ //$('#form-modal').modal('show');
scope.$emit('groupLoaded');
})
.error(function (data, status) {
@@ -803,7 +918,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
scope.formModalActionDisabled = false;
- $('#form-modal').modal('hide');
+ $('#group-modal-dialog').dialog('close');
// Change the selected group
if (groups_reload && parent_scope.selected_tree_id !== tree_id) {
@@ -864,6 +979,12 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
// Cancel
scope.cancelModal = function () {
+ try {
+ $('#group-modal-dialog').dialog('close');
+ }
+ catch(e) {
+ //ignore
+ }
if (scope.searchCleanup) {
scope.searchCleanup();
}
@@ -871,7 +992,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G
};
// Save
- scope.formModalAction = function () {
+ scope.saveGroup = function () {
Wait('start');
var fld, data, json_data;
diff --git a/awx/ui/static/js/helpers/Parse.js b/awx/ui/static/js/helpers/Parse.js
index 432de6d8b0..0ad2d1ad78 100644
--- a/awx/ui/static/js/helpers/Parse.js
+++ b/awx/ui/static/js/helpers/Parse.js
@@ -19,23 +19,21 @@ angular.module('ParseHelper', ['Utilities', 'AngularCodeMirrorModule'])
fld = (params.variable) ? params.variable : 'variables',
pfld = (params.parse_variable) ? params.parse_variable : 'parseType',
onReady = params.onReady,
- onChange = params.onChange,
- codeMirror;
-
+ onChange = params.onChange;
+
function removeField() {
//set our model to the last change in CodeMirror and then destroy CodeMirror
- scope[fld] = codeMirror.getValue();
- codeMirror.destroy();
+ scope[fld] = scope.codeMirror.getValue();
+ scope.codeMirror.destroy();
}
function createField(onChange, onReady) {
//hide the textarea and show a fresh CodeMirror with the current mode (json or yaml)
- codeMirror = AngularCodeMirror();
- codeMirror.addModes($AnsibleConfig.variable_edit_modes);
- codeMirror.showTextArea({ scope: scope, model: fld, element: field_id, mode: scope[pfld], onReady: onReady, onChange: onChange });
+ scope.codeMirror = AngularCodeMirror();
+ scope.codeMirror.addModes($AnsibleConfig.variable_edit_modes);
+ scope.codeMirror.showTextArea({ scope: scope, model: fld, element: field_id, mode: scope[pfld], onReady: onReady, onChange: onChange });
}
-
// Hide the textarea and show a CodeMirror editor
createField(onChange, onReady);
diff --git a/awx/ui/static/js/helpers/Schedules.js b/awx/ui/static/js/helpers/Schedules.js
new file mode 100644
index 0000000000..7668f70b35
--- /dev/null
+++ b/awx/ui/static/js/helpers/Schedules.js
@@ -0,0 +1,245 @@
+/*********************************************
+ * Copyright (c) 2014 AnsibleWorks, Inc.
+ *
+ * Schedules Helper
+ *
+ * Display the scheduler widget in a dialog
+ *
+ */
+
+'use strict';
+
+angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
+
+ .factory('ShowSchedulerModal', ['Wait', function(Wait) {
+ return function(params) {
+ // Set modal dimensions based on viewport width
+
+ var ww, wh, x, y, maxrows, scope = params.scope;
+
+ ww = $(document).width();
+ wh = $('body').height();
+ if (ww > 1199) {
+ // desktop
+ x = 675;
+ y = (625 > wh) ? wh - 20 : 625;
+ maxrows = 20;
+ } else if (ww <= 1199 && ww >= 768) {
+ x = 550;
+ y = (625 > wh) ? wh - 15 : 625;
+ maxrows = 15;
+ } else {
+ x = (ww - 20);
+ y = (625 > wh) ? wh : 625;
+ maxrows = 10;
+ }
+
+ // Create the modal
+ $('#scheduler-modal-dialog').dialog({
+ buttons: {
+ 'Cancel': function() {
+ $(this).dialog('close');
+ },
+ 'Save': function () {
+ setTimeout(function(){
+ scope.$apply(function(){
+ scope.saveSchedule();
+ });
+ });
+ }
+ },
+ modal: true,
+ width: x,
+ height: y,
+ autoOpen: false,
+ create: function () {
+ $('.ui-dialog[aria-describedby="scheduler-modal-dialog"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x');
+ $('.ui-dialog[aria-describedby="scheduler-modal-dialog"]').find('.ui-dialog-buttonset button').each(function () {
+ var c, h, i, l;
+ l = $(this).text();
+ if (l === 'Cancel') {
+ h = "fa-times";
+ c = "btn btn-default";
+ i = "schedule-close-button";
+ $(this).attr({
+ 'class': c,
+ 'id': i
+ }).html("
Cancel");
+ } else if (l === 'Save') {
+ h = "fa-check";
+ c = "btn btn-primary";
+ i = "schedule-save-button";
+ $(this).attr({
+ 'class': c,
+ 'id': i
+ }).html("
Save");
+ }
+ });
+ $('#scheduler-tabs a:first').tab('show');
+ $('#rrule_nlp_description').dblclick(function() {
+ setTimeout(function() { scope.$apply(function() { scope.showRRuleDetail = (scope.showRRuleDetail) ? false : true; }); }, 100);
+ });
+ },
+ resizeStop: function () {
+ // for some reason, after resizing dialog the form and fields (the content) doesn't expand to 100%
+ var dialog = $('.ui-dialog[aria-describedby="status-modal-dialog"]'),
+ content = dialog.find('#scheduler-modal-dialog');
+ content.width(dialog.width() - 28);
+ },
+ close: function () {
+ // Destroy on close
+ $('.tooltip').each(function () {
+ // Remove any lingering tooltip
elements
+ $(this).remove();
+ });
+ $('.popover').each(function () {
+ // remove lingering popover
elements
+ $(this).remove();
+ });
+ $('#scheduler-modal-dialog').dialog('destroy');
+ $('#scheduler-modal-dialog #form-container').empty();
+ },
+ open: function () {
+ Wait('stop');
+ $('#schedulerName').focus();
+ }
+ });
+ };
+ }])
+
+ .factory('ShowDetails', [ function() {
+ return function(params) {
+
+ var scope = params.scope,
+ scheduler = params.scheduler,
+ e = params.e,
+ rrule;
+
+ if ($(e.target).text() === 'Details') {
+ if (scheduler.isValid()) {
+ scope.schedulerIsValid = true;
+ rrule = scheduler.getRRule();
+ scope.occurrence_list = [];
+ scope.dateChoice = 'utc';
+ rrule.all(function(date, i){
+ if (i < 10) {
+ scope.occurrence_list.push({ utc: date.toUTCString(), local: date.toString() });
+ return true;
+ }
+ return false;
+ });
+ scope.rrule_nlp_description = rrule.toText().replace(/^RRule error.*$/,'Natural language description not available');
+ scope.rrule = scheduler.getValue().rrule;
+ }
+ else {
+ scope.schedulerIsValid = false;
+ $('#scheduler-tabs a:first').tab('show');
+ }
+ }
+ };
+ }])
+
+ .factory('EditSchedule', ['SchedulerInit', 'ShowSchedulerModal', 'ShowDetails', 'Wait', 'Rest',
+ function(SchedulerInit, ShowSchedulerModal, ShowDetails, Wait, Rest) {
+ return function(params) {
+ var scope = params.scope,
+ schedule = params.schedule,
+ url = params.url,
+ scheduler;
+
+ Wait('start');
+ $('#form-container').empty();
+ scheduler = SchedulerInit({ scope: scope });
+ scheduler.inject('form-container', false);
+ ShowSchedulerModal({ scope: scope });
+ scope.showRRuleDetail = false;
+
+ setTimeout(function(){
+ $('#scheduler-modal-dialog').dialog('open');
+ scope.$apply(function() {
+ scheduler.setRRule(schedule.rrule);
+ scheduler.setName(schedule.name);
+ });
+ $('#schedulerName').focus();
+ }, 500);
+
+ scope.saveSchedule = function() {
+ var newSchedule;
+ $('#scheduler-tabs a:first').tab('show');
+ 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');
+ $('#scheduler-modal-dialog').dialog('close');
+ })
+ .error(function(){
+ Wait('stop');
+ $('#scheduler-modal-dialog').dialog('close');
+ });
+ }
+ else {
+ scope.schedulerIsValid = false;
+ }
+ };
+
+ $('#scheduler-tabs li a').on('shown.bs.tab', function(e) {
+ ShowDetails({ e: e, scope: scope, scheduler: scheduler });
+ });
+ };
+ }])
+
+ .factory('AddSchedule', ['SchedulerInit', 'ShowSchedulerModal', 'ShowDetails', 'Wait', 'Rest',
+ function(SchedulerInit, ShowSchedulerModal, ShowDetails, Wait, Rest) {
+ return function(params) {
+ var scope = params.scope,
+ url = params.url,
+ schedule = params.schedule,
+ scheduler;
+
+ Wait('start');
+ $('#form-container').empty();
+ scheduler = SchedulerInit({ scope: scope });
+ scheduler.inject('form-container', false);
+ ShowSchedulerModal({ scope: scope });
+ scope.showRRuleDetail = false;
+
+ setTimeout(function(){
+ $('#scheduler-modal-dialog').dialog('open');
+ }, 300);
+
+ scope.saveSchedule = function() {
+ var newSchedule;
+ $('#scheduler-tabs a:first').tab('show');
+ 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');
+ $('#scheduler-modal-dialog').dialog('close');
+ })
+ .error(function(){
+ Wait('stop');
+ $('#scheduler-modal-dialog').dialog('close');
+ });
+ }
+ else {
+ scope.schedulerIsValid = false;
+ }
+ };
+
+ $('#scheduler-tabs li a').on('shown.bs.tab', function(e) {
+ ShowDetails({ e: e, scope: scope, scheduler: scheduler });
+ });
+ };
+ }]);
\ No newline at end of file
diff --git a/awx/ui/static/js/helpers/Variables.js b/awx/ui/static/js/helpers/Variables.js
index 31e3bd7cc5..fb9c65e526 100644
--- a/awx/ui/static/js/helpers/Variables.js
+++ b/awx/ui/static/js/helpers/Variables.js
@@ -23,8 +23,7 @@ angular.module('VariablesHelper', ['Utilities'])
return function (variables) {
var result = "---", json_obj;
if (typeof variables === 'string') {
- if ($.isEmptyObject(variables) || variables === "{}" || variables === "null" ||
- variables === "" || variables === null) {
+ if (variables === "{}" || variables === "null" || variables === "") {
// String is empty, return ---
} else {
try {
@@ -45,13 +44,18 @@ angular.module('VariablesHelper', ['Utilities'])
}
}
else {
- // an object was passed in. just convert to yaml
- try {
- result = jsyaml.safeDump(variables);
+ if ($.isEmptyObject(variables) || variables === null) {
+ // Empty object, return ---
}
- catch(e3) {
- ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!',
- msg: 'Attempt to convert JSON object to YAML document failed: ' + e3.message });
+ else {
+ // convert object to yaml
+ try {
+ result = jsyaml.safeDump(variables);
+ }
+ catch(e3) {
+ ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!',
+ msg: 'Attempt to convert JSON object to YAML document failed: ' + e3.message });
+ }
}
}
return result;
diff --git a/awx/ui/static/js/lists/JobTemplates.js b/awx/ui/static/js/lists/JobTemplates.js
index 62c0b1ea93..903a828ce2 100644
--- a/awx/ui/static/js/lists/JobTemplates.js
+++ b/awx/ui/static/js/lists/JobTemplates.js
@@ -65,7 +65,8 @@ angular.module('JobTemplatesListDefinition', [])
schedule: {
label: 'Schedule',
mode: 'all',
- awToolTip: 'Schedule a future job using this template',
+ ngHref: '#/job_templates/{{ job_template.id }}/schedules',
+ awToolTip: 'Schedule future job template runs',
dataPlacement: 'top'
},
"delete": {
diff --git a/awx/ui/static/js/lists/Projects.js b/awx/ui/static/js/lists/Projects.js
index 9170d085bc..195d5f973a 100644
--- a/awx/ui/static/js/lists/Projects.js
+++ b/awx/ui/static/js/lists/Projects.js
@@ -112,6 +112,13 @@ angular.module('ProjectsListDefinition', [])
ngShow: "project.status == 'updating'",
dataPlacement: 'top'
},
+ schedule: {
+ label: 'Schedule',
+ mode: 'all',
+ ngHref: '#/projects/{{ project.id }}/schedules',
+ awToolTip: 'Schedule future project sync runs',
+ dataPlacement: 'top'
+ },
"delete": {
label: 'Delete',
ngClick: "deleteProject(project.id, project.name)",
diff --git a/awx/ui/static/js/lists/Schedules.js b/awx/ui/static/js/lists/Schedules.js
new file mode 100644
index 0000000000..ff01670cbb
--- /dev/null
+++ b/awx/ui/static/js/lists/Schedules.js
@@ -0,0 +1,64 @@
+/*********************************************
+ * Copyright (c) 2014 AnsibleWorks, Inc.
+ *
+ * Schedules.js
+ * List object for Schedule data model.
+ *
+ */
+
+'use strict';
+
+angular.module('SchedulesListDefinition', [])
+ .value('SchedulesList', {
+
+ name: 'schedules',
+ iterator: 'schedule',
+ selectTitle: '',
+ editTitle: 'Schedules',
+ index: true,
+ hover: true,
+
+ fields: {
+ name: {
+ key: true,
+ label: 'Name'
+ },
+ dtstart: {
+ label: 'Start'
+ },
+ dtend: {
+ label: 'End'
+ }
+ },
+
+ actions: {
+ add: {
+ mode: 'all',
+ ngClick: 'addSchedule()',
+ awToolTip: 'Add a new schedule'
+ },
+ stream: {
+ ngClick: "showActivity()",
+ awToolTip: "View Activity Stream",
+ mode: 'edit'
+ }
+ },
+
+ fieldActions: {
+ edit: {
+ label: 'Edit',
+ ngClick: "editSchedule(schedule.id)",
+ icon: 'icon-edit',
+ awToolTip: 'Edit schedule',
+ dataPlacement: 'top'
+ },
+
+ "delete": {
+ label: 'Delete',
+ ngClick: "deleteSchedule(schedule.id)",
+ icon: 'icon-trash',
+ awToolTip: 'Delete schedule',
+ dataPlacement: 'top'
+ }
+ }
+ });
\ No newline at end of file
diff --git a/awx/ui/static/less/angular-scheduler.less b/awx/ui/static/less/angular-scheduler.less
new file mode 100644
index 0000000000..d81ff45e3b
--- /dev/null
+++ b/awx/ui/static/less/angular-scheduler.less
@@ -0,0 +1,134 @@
+/***************************************************************************
+ * Copyright (c) 2014 Ansible, Inc.
+ *
+ * Styling for angular-scheduler
+ *
+ */
+
+#scheduler-modal-dialog {
+ display: none;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding-top: 25px;
+
+ form {
+ width: 100%;
+ }
+
+ .sublabel {
+ font-weight: normal;
+ }
+
+ #occurrence-label {
+ display: inline-block;
+ }
+
+ #date-choice {
+ display: inline-block;
+ margin-left: 15px;
+ font-size: 12px;
+
+ .label-inline {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ input {
+ margin-bottom: 2px;
+ height: 11px;
+ width: 10px;
+ }
+ .label-inline:first-child {
+ padding-bottom: 2px;
+ margin-right: 10px;
+ }
+ .label-inline:nth-child(3) {
+ margin-right: 10px;
+ }
+ }
+
+ .ui-widget input {
+ font-size: 12px;
+ font-weight: normal;
+ text-align: center;
+ }
+ .ui-spinner.ui-widget-content {
+ border-bottom-color: #ccc;
+ border-top-color: #ccc;
+ border-left-color: #ccc;
+ border-right-color: #ccc;
+ }
+ .ui-spinner-button {
+ border-left-color: #ccc;
+ border-left-style: solid;
+ border-left-width: 1px;
+ }
+ .scheduler-time-spinner {
+ width: 40px;
+ height: 24px;
+ }
+ .scheduler-spinner {
+ width: 50px;
+ height: 24px;
+ }
+ .fmt-help {
+ font-size: 12px;
+ font-weight: normal;
+ color: #999;
+ padding-left: 10px;
+ }
+ .error {
+ color: #dd1b16;
+ font-size: 12px;
+ margin-bottom: 0;
+ margin-top: 0;
+ padding-top: 3px;
+ }
+ .red-text {
+ color: #dd1b16;
+ }
+ input.ng-dirty.ng-invalid, select.ng-dirty.ng-invalid, textarea.ng-dirty.ng-invalid {
+ border: 1px solid red;
+ outline: none;
+ }
+ .help-text {
+ font-size: 12px;
+ font-weight: normal;
+ color: #999;
+ margin-top: 5px;
+ }
+ .inline-label {
+ margin-left: 10px;
+ }
+ #scheduler-buttons {
+ margin-top: 20px;
+ }
+ .no-label {
+ padding-top: 25px;
+ }
+ .padding-top-slim {
+ padding-top: 5px;
+ }
+ .option-pad-left {
+ padding-left: 15px;
+ }
+ .option-pad-top {
+ padding-top: 15px;
+ }
+ .option-pad-bottom {
+ padding-bottom: 15px;
+ }
+ #monthlyOccurrence, #monthlyWeekDay {
+ margin-top: 5px;
+ }
+ select {
+ width: 100%;
+ }
+ .occurrence-list {
+ border: 1px solid @well-border;
+ padding: 8px 10px;
+ border-radius: 4px;
+ background-color: @well;
+ list-style: none;
+ margin-bottom: 5px;
+ }
+}
\ No newline at end of file
diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less
index 32d86e129e..36855f89aa 100644
--- a/awx/ui/static/less/ansible-ui.less
+++ b/awx/ui/static/less/ansible-ui.less
@@ -31,6 +31,7 @@
@import "animations.less";
@import "jquery-ui-overrides.less";
@import "codemirror.less";
+@import "angular-scheduler.less";
html, body { height: 100%; }
@@ -527,6 +528,9 @@ dd {
padding-right: 10px;
}
+#group_form #group_tabs {
+ margin-top: 25px;
+}
/* Outline required fields in Red when there is an error */
.form-control.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid:focus {
@@ -794,6 +798,16 @@ input[type="checkbox"].checkbox-no-label {
margin-top: 10px;
}
+.checkbox-group {
+ .radio-inline + .radio-inline,
+ .checkbox-inline + .checkbox-inline {
+ margin-left: 0;
+ }
+ .checkbox-inline, .radio-inline {
+ margin-right: 10px;
+ }
+}
+
.checkbox-options {
font-weight: normal;
padding-right: 20px;
diff --git a/awx/ui/static/less/jquery-ui-overrides.less b/awx/ui/static/less/jquery-ui-overrides.less
index d96f42a88a..d9f2feedf1 100644
--- a/awx/ui/static/less/jquery-ui-overrides.less
+++ b/awx/ui/static/less/jquery-ui-overrides.less
@@ -9,6 +9,10 @@
*/
+table.ui-datepicker-calendar {
+ background-color: @well;
+}
+
/* Modal dialog */
.ui-dialog-title {
diff --git a/awx/ui/static/lib/angular-scheduler/.bower.json b/awx/ui/static/lib/angular-scheduler/.bower.json
index abec77262b..a7b9a307fa 100644
--- a/awx/ui/static/lib/angular-scheduler/.bower.json
+++ b/awx/ui/static/lib/angular-scheduler/.bower.json
@@ -1,5 +1,6 @@
{
"name": "angular-scheduler",
+ "version": "0.0.2",
"authors": [
"Chris Houseknecht
"
],
@@ -35,14 +36,13 @@
"rrule",
"calendar"
],
- "_release": "f2488ff1ec",
+ "_release": "0.0.2",
"_resolution": {
- "type": "branch",
- "branch": "master",
- "commit": "f2488ff1ec1b2aa48206fa97111b6f8d5e88de89"
+ "type": "version",
+ "tag": "v0.0.2",
+ "commit": "f9df5d081112d0ebecfd418bd859c26f0c7711b4"
},
"_source": "git://github.com/chouseknecht/angular-scheduler.git",
"_target": "*",
- "_originalSource": "angular-scheduler",
- "_direct": true
+ "_originalSource": "angular-scheduler"
}
\ No newline at end of file
diff --git a/awx/ui/static/lib/angular-scheduler/app/css/sampleApp.less b/awx/ui/static/lib/angular-scheduler/app/css/sampleApp.less
index a427c3e0b6..371dd50cd0 100644
--- a/awx/ui/static/lib/angular-scheduler/app/css/sampleApp.less
+++ b/awx/ui/static/lib/angular-scheduler/app/css/sampleApp.less
@@ -60,6 +60,20 @@ textarea {
resize: none;
}
+.text-left {
+ text-align: left;
+}
+
+.occurrence-list {
+ font-size: 12px;
+}
+
+#rrule-description {
+ font-size: 14px;
+ font-weight: normal;
+ text-align: left;
+}
+
a,
a:active,
a:link,
@@ -143,7 +157,7 @@ a:hover {
color: #A9A9A9;
}
.mono-space {
- font-family: Fixed, monospace;
+ font-family: "Courier New", Courier, monospace;
}
textarea.resizable {
resize: vertical;
diff --git a/awx/ui/static/lib/angular-scheduler/app/index.html b/awx/ui/static/lib/angular-scheduler/app/index.html
index 71a1677ece..1bd879e1c1 100644
--- a/awx/ui/static/lib/angular-scheduler/app/index.html
+++ b/awx/ui/static/lib/angular-scheduler/app/index.html
@@ -51,6 +51,7 @@
+
diff --git a/awx/ui/static/lib/angular-scheduler/app/js/sampleApp.js b/awx/ui/static/lib/angular-scheduler/app/js/sampleApp.js
index 027da262db..43d217d049 100644
--- a/awx/ui/static/lib/angular-scheduler/app/js/sampleApp.js
+++ b/awx/ui/static/lib/angular-scheduler/app/js/sampleApp.js
@@ -31,9 +31,6 @@ angular.module('sampleApp', ['ngRoute', 'AngularScheduler', 'Timezones'])
scheduler.inject('form-container', true);
- console.log('User timezone: ');
- console.log(scheduler.getUserTimezone());
-
$scope.setRRule = function() {
$scope.inputRRuleMsg = '';
$scope.inputRRuleMsg = scheduler.setRRule($scope.inputRRule);
@@ -44,19 +41,44 @@ angular.module('sampleApp', ['ngRoute', 'AngularScheduler', 'Timezones'])
$scope.saveForm = function() {
if (scheduler.isValid()) {
var schedule = scheduler.getValue(),
- html =
- "\n",
+ rrule = scheduler.getRRule(),
+ html,
wheight = $(window).height(),
wwidth = $(window).width(),
- w, h;
-
+ w, h, occurrences;
+
+ occurrences = [];
+ rrule.all(function(date, i) {
+ if (i < 10) {
+ occurrences.push(date);
+ return true;
+ }
+ else {
+ return false;
+ }
+ });
+
+ html = "\n";
+
w = (600 > wwidth) ? wwidth : 600;
- h = (400 > wheight) ? wheight : 400;
+ h = (600 > wheight) ? wheight : 600;
$('#message').html(html)
.dialog({
diff --git a/awx/ui/static/lib/angular-scheduler/bower.json b/awx/ui/static/lib/angular-scheduler/bower.json
index 2d51b9f160..e7e6738491 100644
--- a/awx/ui/static/lib/angular-scheduler/bower.json
+++ b/awx/ui/static/lib/angular-scheduler/bower.json
@@ -1,6 +1,6 @@
{
"name": "angular-scheduler",
- "version": "0.0.1",
+ "version": "0.0.2",
"authors": [
"Chris Houseknecht "
],
diff --git a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.css b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.css
index 40b67dc329..a5c4bd53fc 100644
--- a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.css
+++ b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.css
@@ -47,9 +47,10 @@
margin-top: 0;
padding-top: 3px;
}
-.pull-up {
- margin-top: -15px;
- margin-bottom: 10px;
+.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.html b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.html
index f87af43cb6..b8a3251419 100644
--- a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.html
+++ b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.html
@@ -15,16 +15,13 @@