Inventory group edit dialog is now draggable and resizable. Added tab for schedules fixed issue with Variables.js helper- when json variables object empty, return --- rather than {}

This commit is contained in:
Chris Houseknecht
2014-03-12 12:14:08 -04:00
parent 8960f17f28
commit 199f091a1c
31 changed files with 1381 additions and 99 deletions

View File

@@ -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',

View File

@@ -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'
];

View File

@@ -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: { }

View File

@@ -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'
}
}
}
}

View File

@@ -129,9 +129,9 @@ angular.module('ProjectFormDefinition', [])
hdr: 'GIT URLs',
content: '<p>Example URLs for GIT SCM include:</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
'<p><strong>Note:</strong> If using SSH protocol for GitHub or Bitbucket, enter in the SSH key only, ' +
'<p><strong>Note:</strong> 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'
}
}
}
}

View File

@@ -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 = "<div id=\"group-modal-dialog\" title=\"Group Edit\">\n" +
"<div id=\"form-container\" style=\"width: 100%;\"></div></div>\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("<i class=\"fa " + h + "\"></i> Cancel");
} else if (l === 'Save') {
h = "fa-check";
c = "btn btn-primary";
i = "group-save-button";
$(this).attr({
'class': c,
'id': i
}).html("<i class=\"fa " + h + "\"></i> 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 <div> elements
$(this).remove();
});
$('.popover').each(function () {
// remove lingering popover <div> 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;

View File

@@ -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);

View File

@@ -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("<i class=\"fa " + h + "\"></i> Cancel");
} else if (l === 'Save') {
h = "fa-check";
c = "btn btn-primary";
i = "schedule-save-button";
$(this).attr({
'class': c,
'id': i
}).html("<i class=\"fa " + h + "\"></i> 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 <div> elements
$(this).remove();
});
$('.popover').each(function () {
// remove lingering popover <div> 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 });
});
};
}]);

View File

@@ -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;

View File

@@ -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": {

View File

@@ -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)",

View File

@@ -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'
}
}
});

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -9,6 +9,10 @@
*/
table.ui-datepicker-calendar {
background-color: @well;
}
/* Modal dialog */
.ui-dialog-title {

View File

@@ -1,5 +1,6 @@
{
"name": "angular-scheduler",
"version": "0.0.2",
"authors": [
"Chris Houseknecht <chouse@ansible.com>"
],
@@ -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"
}

View File

@@ -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;

View File

@@ -51,6 +51,7 @@
<!-- rrule -->
<script src="/bower_components/underscore/underscore.js"></script>
<script src="/bower_components/rrule/lib/rrule.js"></script>
<script src="/bower_components/rrule/lib/nlp.js"></script>
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/angular-route/angular-route.min.js"></script>

View File

@@ -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 =
"<form>\n" +
"<div class=\"form-group\">\n" +
"<label>RRule</label>\n" +
"<textarea id=\"rrule-result\" readonly class=\"form-control\" rows=\"8\">" + schedule.rrule + "</textarea>\n" +
"</div>\n" +
"</form>\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 = "<form>\n" +
"<div class=\"form-group\">\n" +
"<label>Description</label>\n" +
"<textarea id=\"rrule-description\" readonly class=\"form-control\" rows=\"2\">Run " + rrule.toText() + "</textarea>\n" +
"</div>" +
"<div class=\"form-group\">\n" +
"<label>RRule</label>\n" +
"<textarea id=\"rrule-result\" readonly class=\"form-control\" rows=\"3\">" + schedule.rrule + "</textarea>\n" +
"</div>\n" +
"<div class=\"form-group\">\n" +
"<label>Occurrences (up to 10)</label>\n" +
"<ul class=\"occurrence-list mono-space\">\n";
occurrences.forEach(function(itm){
html += "<li>" + itm + "</li>\n";
});
html += "</ul>\n" +
"</div>\n" +
"</form>\n";
w = (600 > wwidth) ? wwidth : 600;
h = (400 > wheight) ? wheight : 400;
h = (600 > wheight) ? wheight : 600;
$('#message').html(html)
.dialog({

View File

@@ -1,6 +1,6 @@
{
"name": "angular-scheduler",
"version": "0.0.1",
"version": "0.0.2",
"authors": [
"Chris Houseknecht <chouse@ansible.com>"
],

View File

@@ -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;

View File

@@ -15,16 +15,13 @@
<div class="col-md-12">
<form class="form" role="form" name="scheduler_form" novalidate>
<div class="row">
<div class="col-md-5">
<div class="form-group">
<label><span class="red-text">*</span> Name</label>
<input type="text" class="form-control input-sm" name="schedulerName" id="schedulerName" ng-model="schedulerName" required placeholder="Schedule name">
<div class="error" ng-show="scheduler_form.schedulerName.$dirty && scheduler_form.schedulerName.$error.required">Schedule name is required</div>
</div>
</div>
<div class="form-group">
<label><span class="red-text">*</span> Name</label>
<input type="text" class="form-control input-sm" name="schedulerName" id="schedulerName" ng-model="schedulerName" required placeholder="Schedule name">
<div class="error" ng-show="scheduler_form.schedulerName.$dirty && scheduler_form.schedulerName.$error.required">Schedule name is required</div>
</div>
<div class="row">
<div class="col-md-5">
<div class="form-group">
@@ -53,10 +50,10 @@
</div>
</div>
</div>
<div class="row">
<div class="row error-pull-up">
<div class="col-md-12">
<div class="error pull-up" ng-show="scheduler_form.schedulerStartDt.$dirty && scheduler_form_schedulerStartDt_error" ng-bind="scheduler_form_schedulerStartDt_error"></div>
<div class="error" ng-show="scheduler_form_schedulerStartDt_error" ng-bind="scheduler_form_schedulerStartDt_error"></div>
</div>
</div>
@@ -79,7 +76,7 @@
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label>Repeat</label>
<label>Repeat frequency</label>
<select name="schedulerFrequency" id="schedulerFrequency" ng-model="schedulerFrequency"
ng-options="f.name for f in frequencyOptions" required class="form-control input-sm"
ng-change="scheduleRepeatChange()"></select>
@@ -226,4 +223,4 @@
</div>
</div><!-- col-md-12 -->
</div><!-- row -->
</div><!-- row -->

View File

@@ -290,14 +290,14 @@ angular.module('AngularScheduler', ['underscore'])
}
}
else {
scope.startDateError("Provide a valid start date and time");
this.scope.startDateError("Provide a valid start date and time");
validity = false;
}
return validity;
};
// Returns an rrule object
this.getRule = function() {
this.getRRule = function() {
var options = this.getOptions();
return GetRule(options);
};
@@ -305,7 +305,7 @@ angular.module('AngularScheduler', ['underscore'])
// Return object containing schedule name, string representation of rrule per iCalendar RFC,
// and options used to create rrule
this.getValue = function() {
var rule = this.getRule(),
var rule = this.getRRule(),
options = this.getOptions();
return {
name: scope.schedulerName,
@@ -319,6 +319,10 @@ angular.module('AngularScheduler', ['underscore'])
return SetRule(rule, this.scope);
};
this.setName = function(name) {
this.scope.schedulerName = name;
};
// Read in the HTML partial, compile and inject it into the DOM.
// Pass in the target element's id attribute value or an angular.element()
// object.
@@ -336,13 +340,13 @@ angular.module('AngularScheduler', ['underscore'])
// Get the user's local timezone
this.getUserTimezone = function() {
return $timezones.getLocal();
}
};
};
return new fn();
};
}])
.factory('Inject', ['AngularScheduler.partial', '$compile', '$http', '$log', function(scheduler_partial, $compile, $http, $log) {
.factory('Inject', ['AngularScheduler.partial', '$compile', '$http', '$log', function(scheduler_partial, $compile, $http) {
return function(params) {
var scope = params.scope,
@@ -741,12 +745,12 @@ angular.module('AngularScheduler', ['underscore'])
scope.frequencyOptions = [
{ name: 'None (run once)', value: 'none', intervalLabel: '' },
{ name: 'Minutely', value: 'minutely', intervalLabel: 'minutes' },
{ name: 'Hourly', value: 'hourly', intervalLabel: 'hours' },
{ name: 'Daily', value: 'daily', intervalLabel: 'days' },
{ name: 'Weekly', value: 'weekly', intervalLabel: 'weeks' },
{ name: 'Monthly', value: 'monthly', intervalLabel: 'months' },
{ name: 'Yearly', value: 'yearly', intervalLabel: 'years' }
{ name: 'Minute', value: 'minutely', intervalLabel: 'minutes' },
{ name: 'Hour', value: 'hourly', intervalLabel: 'hours' },
{ name: 'Day', value: 'daily', intervalLabel: 'days' },
{ name: 'Week', value: 'weekly', intervalLabel: 'weeks' },
{ name: 'Month', value: 'monthly', intervalLabel: 'months' },
{ name: 'Year', value: 'yearly', intervalLabel: 'years' }
];
scope.endOptions = [
@@ -830,6 +834,11 @@ angular.module('AngularScheduler', ['underscore'])
options.maxDate = (attrs.maxDate) ? new Date(attrs('maxDate')) : null;
options.changeMonth = (attrs.changeMonth === "false") ? false : true;
options.changeYear = (attrs.changeYear === "false") ? false : true;
options.beforeShow = function() {
setTimeout(function(){
$('.ui-datepicker').css('z-index', 9999);
}, 100);
};
$(element).datepicker(options);
}
};

View File

@@ -1 +1 @@
.ui-widget input{font-size:12px;font-weight:400;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:400;color:#999;padding-left:10px}.error{color:#dd1b16;font-size:12px;margin-bottom:0;margin-top:0;padding-top:3px}.pull-up{margin-top:-15px;margin-bottom:10px}.red-text{color:#dd1b16}input.ng-dirty.ng-invalid,select.ng-dirty.ng-invalid,textarea.ng-dirty.ng-invalid{border:1px solid red;outline:0}.help-text{font-size:12px;font-weight:400;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%}
.ui-widget input{font-size:12px;font-weight:400;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:400;color:#999;padding-left:10px}.error{color:#dd1b16;font-size:12px;margin-bottom:0;margin-top:0;padding-top:3px}.error-pull-up{position:relative;top:-15px;margin-bottom:15px}.red-text{color:#dd1b16}input.ng-dirty.ng-invalid,select.ng-dirty.ng-invalid,textarea.ng-dirty.ng-invalid{border:1px solid red;outline:0}.help-text{font-size:12px;font-weight:400;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%}

File diff suppressed because one or more lines are too long

View File

@@ -1162,8 +1162,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities'])
html += " class=\"active\"";
}
html += "><a id=\"" + tab.name + "_link\" ng-click=\"toggleTab($event, '" + tab.name + "_link', '" +
this.form.name + "_tabs')\" href=\"#" + tab.name + "\"" +
tab.name + "\" data-toggle=\"tab\">" + tab.label + "</a></li>\n";
this.form.name + "_tabs')\" href=\"#" + tab.name + "\" data-toggle=\"tab\">" + tab.label + "</a></li>\n";
}
html += "</ul>\n";
html += "<div class=\"tab-content\">\n";
@@ -1396,7 +1395,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities'])
}
html += "\"></i></a></th>\n";
}
html += "<th></th>\n";
html += "<th>Actions</th>\n";
html += "</tr>\n";
html += "</thead>";
html += "<tbody>\n";

View File

@@ -0,0 +1,52 @@
<div class="row">
<div class="col-lg-12" id="breadcrumbs"></div>
</div>
<div class="row">
<div class="col-sm-12">
<div id="schedule-list-target"></div>
</div>
</div>
<div id="scheduler-modal-dialog" title="Edit Schedule">
<ul id="scheduler-tabs" class="nav nav-tabs">
<li class="active"><a href="#schedule" id="schedule-link" data-toggle="tab" ng-click="toggleTab($event, 'schedule-link', 'scheduler-tabs')">Options</a></li>
<li><a href="#occurrences" id="occurrence-link" data-toggle="tab" ng-click="toggleTab($event, 'occurrence-link', 'scheduler-tabs')">Details</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="schedule">
<div id="form-container"></div>
</div>
<div class="tab-pane" id="occurrences">
<div class="alert alert-danger" ng-show="!schedulerIsValid">
<p>The scheduler options are invalid or incomplete. Make the needed changes on the options tab, then come back here to see details.</p>
</div>
<div ng-show="schedulerIsValid">
<div class="form-group">
<label>Description</label>
<textarea ng-model="rrule_nlp_description" name="rrule_nlp_description" id="rrule_nlp_description" readonly class="form-control" rows="2"></textarea>
</div>
<div class="form-group" ng-show="showRRuleDetail">
<label>RRule</label>
<textarea ng-model="rrule" name="rrule" id="rrule" readonly class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<label id="occurrences-label">Occurrences <span class="sublabel">(limited to first 10)</label>
<div id="date-choice">
<div class="label-inline"><strong>Date format</strong></div>
<input type="radio" ng-model="dateChoice" id="date-choice-utc" value="utc" >
<div class="label-inline"> UTC</div>
<input type="radio" ng-model="dateChoice" id="date-choice-local" value="local" >
<div class="label-inline"> Local time</div>
</div>
<ul class="occurrence-list mono-space" ng-show="dateChoice == 'utc'">
<li ng-repeat="occurrence in occurrence_list">{{ occurrence.utc }}</li>
</ul>
<ul class="occurrence-list mono-space" ng-show="dateChoice == 'local'">
<li ng-repeat="occurrence in occurrence_list">{{ occurrence.local }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,40 @@
{
"count": 3,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"job_template": 3,
"inventory": null,
"project": null,
"job_class": "ansible:playbook",
"name": "Hourly",
"dtstart": "2014-03-10T17:00:00.000Z" ,
"dtend": null,
"rrule": "FREQ=HOURLY;DTSTART=20140310T170000Z;INTERVAL=1"
},
{
"id": 2,
"job_template": 3,
"inventory": null,
"project": null,
"job_class": "ansible:playbook",
"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": 3,
"inventory": null,
"project": null,
"job_class": "ansible:playbook",
"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"
}
]
}

View File

@@ -0,0 +1,43 @@
{
"count": 3,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"job_template": null,
"inventory": null,
"project": 1,
"job_class": "project:sync",
"job_type": "project_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": null,
"project": 1,
"job_class": "project:sync",
"job_type": "project_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": null,
"project": 1,
"job_class": "project:sync",
"job_type": "project_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"
}
]
}

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env node
var util = require('util'),
http = require('http'),
fs = require('fs'),
url = require('url'),
events = require('events');
var DEFAULT_PORT = 8000;
function main(argv) {
new HttpServer({
'GET': createServlet(StaticServlet),
'HEAD': createServlet(StaticServlet)
}).start(Number(argv[2]) || DEFAULT_PORT);
}
function escapeHtml(value) {
return value.toString().
replace('<', '&lt;').
replace('>', '&gt;').
replace('"', '&quot;');
}
function createServlet(Class) {
var servlet = new Class();
return servlet.handleRequest.bind(servlet);
}
/**
* An Http server implementation that uses a map of methods to decide
* action routing.
*
* @param {Object} Map of method => Handler function
*/
function HttpServer(handlers) {
this.handlers = handlers;
this.server = http.createServer(this.handleRequest_.bind(this));
}
HttpServer.prototype.start = function(port) {
this.port = port;
this.server.listen(port);
util.puts('Http Server running at http://localhost:' + port + '/');
};
HttpServer.prototype.parseUrl_ = function(urlString) {
var parsed = url.parse(urlString);
parsed.pathname = url.resolve('/', parsed.pathname);
return url.parse(url.format(parsed), true);
};
HttpServer.prototype.handleRequest_ = function(req, res) {
var logEntry = req.method + ' ' + req.url;
if (req.headers['user-agent']) {
logEntry += ' ' + req.headers['user-agent'];
}
util.puts(logEntry);
req.url = this.parseUrl_(req.url);
var handler = this.handlers[req.method];
if (!handler) {
res.writeHead(501);
res.end();
} else {
handler.call(this, req, res);
}
};
/**
* Handles static content.
*/
function StaticServlet() {}
StaticServlet.MimeMap = {
'txt': 'text/plain',
'html': 'text/html',
'css': 'text/css',
'xml': 'application/xml',
'json': 'application/json',
'js': 'application/javascript',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'png': 'image/png',
  'svg': 'image/svg+xml'
};
StaticServlet.prototype.handleRequest = function(req, res) {
var self = this;
var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/g, function(match, hex){
return String.fromCharCode(parseInt(hex, 16));
});
var parts = path.split('/');
if (parts[parts.length-1].charAt(0) === '.')
return self.sendForbidden_(req, res, path);
fs.stat(path, function(err, stat) {
if (err)
return self.sendMissing_(req, res, path);
if (stat.isDirectory())
return self.sendDirectory_(req, res, path);
return self.sendFile_(req, res, path);
});
}
StaticServlet.prototype.sendError_ = function(req, res, error) {
res.writeHead(500, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>Internal Server Error</title>\n');
res.write('<h1>Internal Server Error</h1>');
res.write('<pre>' + escapeHtml(util.inspect(error)) + '</pre>');
util.puts('500 Internal Server Error');
util.puts(util.inspect(error));
};
StaticServlet.prototype.sendMissing_ = function(req, res, path) {
path = path.substring(1);
res.writeHead(404, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>404 Not Found</title>\n');
res.write('<h1>Not Found</h1>');
res.write(
'<p>The requested URL ' +
escapeHtml(path) +
' was not found on this server.</p>'
);
res.end();
util.puts('404 Not Found: ' + path);
};
StaticServlet.prototype.sendForbidden_ = function(req, res, path) {
path = path.substring(1);
res.writeHead(403, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>403 Forbidden</title>\n');
res.write('<h1>Forbidden</h1>');
res.write(
'<p>You do not have permission to access ' +
escapeHtml(path) + ' on this server.</p>'
);
res.end();
util.puts('403 Forbidden: ' + path);
};
StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) {
res.writeHead(301, {
'Content-Type': 'text/html',
'Location': redirectUrl
});
res.write('<!doctype html>\n');
res.write('<title>301 Moved Permanently</title>\n');
res.write('<h1>Moved Permanently</h1>');
res.write(
'<p>The document has moved <a href="' +
redirectUrl +
'">here</a>.</p>'
);
res.end();
util.puts('301 Moved Permanently: ' + redirectUrl);
};
StaticServlet.prototype.sendFile_ = function(req, res, path) {
var self = this;
var file = fs.createReadStream(path);
res.writeHead(200, {
'Content-Type': StaticServlet.
MimeMap[path.split('.').pop()] || 'text/plain'
});
if (req.method === 'HEAD') {
res.end();
} else {
file.on('data', res.write.bind(res));
file.on('close', function() {
res.end();
});
file.on('error', function(error) {
self.sendError_(req, res, error);
});
}
};
StaticServlet.prototype.sendDirectory_ = function(req, res, path) {
var self = this;
if (path.match(/[^\/]$/)) {
req.url.pathname += '/';
var redirectUrl = url.format(url.parse(url.format(req.url)));
return self.sendRedirect_(req, res, redirectUrl);
}
fs.readdir(path, function(err, files) {
if (err)
return self.sendError_(req, res, error);
if (!files.length)
return self.writeDirectoryIndex_(req, res, path, []);
var remaining = files.length;
files.forEach(function(fileName, index) {
fs.stat(path + '/' + fileName, function(err, stat) {
if (err)
return self.sendError_(req, res, err);
if (stat.isDirectory()) {
files[index] = fileName + '/';
}
if (!(--remaining))
return self.writeDirectoryIndex_(req, res, path, files);
});
});
});
};
StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) {
path = path.substring(1);
res.writeHead(200, {
'Content-Type': 'text/html'
});
if (req.method === 'HEAD') {
res.end();
return;
}
res.write('<!doctype html>\n');
res.write('<title>' + escapeHtml(path) + '</title>\n');
res.write('<style>\n');
res.write(' ol { list-style-type: none; font-size: 1.2em; }\n');
res.write('</style>\n');
res.write('<h1>Directory: ' + escapeHtml(path) + '</h1>');
res.write('<ol>');
files.forEach(function(fileName) {
if (fileName.charAt(0) !== '.') {
res.write('<li><a href="' +
escapeHtml(fileName) + '">' +
escapeHtml(fileName) + '</a></li>');
}
});
res.write('</ol>');
res.end();
};
// Must be last,
main(process.argv);

View File

@@ -33,6 +33,16 @@
<script src="{{ STATIC_URL }}lib/angular-sanitize/angular-sanitize.min.js"></script>
<script src="{{ STATIC_URL }}lib/angular-md5/angular-md5.min.js"></script>
<script src="{{ STATIC_URL }}lib/angular-codemirror/lib/AngularCodeMirror.js"></script>
<!-- scheduler pieces -->
<script src="{{ STATIC_URL }}lib/timezone-js/src/date.js"></script>
<script src="{{ STATIC_URL }}lib/angular-tz-extensions/packages/jstimezonedetect/jstz.min.js"></script>
<script src="{{ STATIC_URL }}lib/underscore/underscore.js"></script>
<script src="{{ STATIC_URL }}lib/rrule/lib/rrule.js"></script>
<script src="{{ STATIC_URL }}lib/rrule/lib/nlp.js"></script>
<script src="{{ STATIC_URL }}lib/angular-tz-extensions/lib/angular-tz-extensions.js"></script>
<script src="{{ STATIC_URL }}lib/angular-scheduler/lib/angular-scheduler.js"></script>
{% if settings.USE_MINIFIED_JS %}
<script src="{{ STATIC_URL }}js/awx.min.js"></script>
{% else %}
@@ -66,6 +76,7 @@
<script src="{{ STATIC_URL }}js/controllers/JobEvents.js"></script>
<script src="{{ STATIC_URL }}js/controllers/JobHosts.js"></script>
<script src="{{ STATIC_URL }}js/controllers/Permissions.js"></script>
<script src="{{ STATIC_URL }}js/controllers/Schedules.js"></script>
<script src="{{ STATIC_URL }}js/forms/Users.js"></script>
<script src="{{ STATIC_URL }}js/forms/Organizations.js"></script>
<script src="{{ STATIC_URL }}js/forms/Inventories.js"></script>
@@ -104,6 +115,7 @@
<script src="{{ STATIC_URL }}js/lists/HomeHosts.js"></script>
<script src="{{ STATIC_URL }}js/lists/Groups.js"></script>
<script src="{{ STATIC_URL }}js/lists/Hosts.js"></script>
<script src="{{ STATIC_URL }}js/lists/Schedules.js"></script>
<script src="{{ STATIC_URL }}js/helpers/refresh-related.js"></script>
<script src="{{ STATIC_URL }}js/helpers/related-search.js"></script>
<script src="{{ STATIC_URL }}js/helpers/refresh.js"></script>
@@ -128,6 +140,7 @@
<script src="{{ STATIC_URL }}js/helpers/Groups.js"></script>
<script src="{{ STATIC_URL }}js/helpers/Hosts.js"></script>
<script src="{{ STATIC_URL }}js/helpers/Variables.js"></script>
<script src="{{ STATIC_URL }}js/helpers/Schedules.js"></script>
<script src="{{ STATIC_URL }}js/widgets/JobStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/InventorySyncStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/SCMSyncStatus.js"></script>
@@ -401,7 +414,7 @@
<script src="{{ STATIC_URL }}lib/codemirror/addon/edit/closebrackets.js"></script>
<script src="{{ STATIC_URL }}lib/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ STATIC_URL }}lib/codemirror/addon/selection/active-line.js"></script>
<script>
// When user clicks on main tab, fire the matching Angular route
$('a[data-toggle="tab"]').on('show.bs.tab', function (e) {