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:

' + - '

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" + + "
\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" + - "
\n" + - "\n" + - "\n" + - "
\n" + - "
\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" + + "
\n" + + "\n" + + "\n" + + "
" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
    \n"; + occurrences.forEach(function(itm){ + html += "
  • " + itm + "
  • \n"; + }); + html += "
\n" + + "
\n" + + "
\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 @@
-
-
-
- - -
Schedule name is required
-
-
+ +
+ + +
Schedule name is required
- +
@@ -53,10 +50,10 @@
- -
+ +
-
+
@@ -79,7 +76,7 @@
- + @@ -226,4 +223,4 @@
-
\ No newline at end of file +
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 42cfc8348d..8e4b92e8be 100644 --- a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.js +++ b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.js @@ -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); } }; diff --git a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.css b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.css index b8e12ff829..8f38cffc2f 100644 --- a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.css +++ b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.css @@ -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%} \ No newline at end of file +.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%} \ No newline at end of file diff --git a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.js b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.js index d4c0e90a37..967c23c774 100644 --- a/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.js +++ b/awx/ui/static/lib/angular-scheduler/lib/angular-scheduler.min.js @@ -1 +1 @@ -/*! angular-schedule.js - v0.0.1 - 2014-02-26 */"use strict";angular.module("AngularScheduler",["Timezones"]).constant("$timezones.definitions.location","/bower_components/angular-timezones/tz/data").constant("scheduler_partial","/lib/angular-scheduler.html").factory("SchedulerInit",["$filter","$timezones","LoadLookupValues","SetDefaults","CreateObject",function(a,b,c,d,e){return function(a){var f=a.scope;return f.removeZonesReady&&f.removeZonesReady(),f.removeZonesReady=f.$on("zonesReady",function(){var a;if(f.timeZones=JSON.parse(localStorage.zones),f.current_timezone=b.getLocal(),!$.isEmptyObject(f.current_timezone)&&f.current_timezone.name)for(a=0;a=0?f.weekDays.splice(a,1):f.weekDays.push(a)},f.startDateError=function(a){f.scheduler_form&&(f.scheduler_form_schedulerStartDt_error=a,f.scheduler_form.schedulerStartDt.$pristine=!1,f.scheduler_form.schedulerStartDt.$dirty=!0,$("#schedulerStartDt").removeClass("ng-pristine").removeClass("ng-valid").removeClass("ng-valid-custom-error").addClass("ng-dirty").addClass("ng-invalid").addClass("ng-invalid-custom-error"))},f.resetStartDate=function(){f.scheduler_form&&(f.scheduler_form_schedulerStartDt_error="",f.scheduler_form.schedulerStartDt.$setValidity("custom-error",!0),f.scheduler_form.schedulerStartDt.$setPristine())},c(f),d(f),f.scheduleTimeChange(),e(f)}}]).factory("CreateObject",["$filter","GetRule","Inject","SetDefaults","$timezones",function(a,b,c,d,e){return function(f){var g=function(){this.scope=f,this.getOptions=function(){var a={};return a.startDate=this.scope.schedulerUTCTime,a.frequency=this.scope.schedulerFrequency.value,a.interval=this.scope.schedulerInterval,"after"===this.scope.schedulerEnd.value&&(a.occurrenceCount=this.scope.schedulerOccurrenceCount),"on"===this.scope.schedulerEnd.value&&(a.endDate=this.scope.schedulerEndDt),"weekly"===this.scope.schedulerFrequency.value?a.weekDays=this.scope.weekDays:"yearly"===this.scope.schedulerFrequency.value?"month"===this.scope.yearlyRepeatOption?(a.month=this.scope.yearlyMonth.value,a.monthDay=this.scope.yearlyMonthDay):(a.setOccurrence=this.scope.yearlyOccurrence.value,a.weekDays=this.scope.yearlyWeekDay.value,a.month=this.scope.yearlyOtherMonth.value):"monthly"===this.scope.schedulerFrequency.value&&("day"===this.scope.monthlyRepeatOption?a.monthDay=this.scope.monthDay:(a.setOccurrence=this.scope.monthlyOccurrence.value,a.weekDays=this.scope.monthlyWeekDay.value)),a},this.clearErrors=function(){this.scope.scheduler_weekDays_error=!1,this.scope.scheduler_endDt_error=!1,this.scope.resetStartDate(),this.scope.scheduler_endDt_error=!1,this.scope.scheduler_form.schedulerEndDt.$setValidity("custom-error",!0),this.scope.scheduler_form.schedulerEndDt.$setPristine(),this.scope.scheduler_form.$setPristine()},this.isValid=function(){var b,c,d,g,h=!0;return this.clearErrors(),"weekly"===this.scope.schedulerFrequency.value&&0===f.weekDays.length&&(this.scope.scheduler_weekDays_error=!0,h=!1),this.scope.scheduler_form.scheduleName.$valid||(this.scope.scheduler_form.scheduleName.$dirty=!0,$("#scheduleName").addClass("ng-dirty"),h=!1),"on"===this.scope.schedulerEnd.value&&(/^\d{4}-\d{2}-\d{2}$/.test(this.scope.schedulerEndDt)||(this.scope.scheduler_form.schedulerEndDt.$pristine=!1,this.scope.scheduler_form.schedulerEndDt.$dirty=!0,$("#schedulerEndDt").removeClass("ng-pristine").removeClass("ng-valid").removeClass("ng-valid-custom-error").addClass("ng-dirty").addClass("ng-invalid").addClass("ng-invalid-custom-error"),this.scope.scheduler_endDt_error=!0,h=!1)),this.scope.schedulerUTCTime?(b=new Date(this.scope.schedulerUTCTime),c=new Date,d=c.getFullYear()+"-"+a("schZeroPad")(c.getMonth()+1,2)+"-"+a("schZeroPad")(c.getDate(),2)+"T"+a("schZeroPad")(c.getHours(),2)+":"+a("schZeroPad")(c.getMinutes(),2)+":"+a("schZeroPad")(c.getSeconds(),2)+".000Z",g=e.toUTC(d,this.scope.schedulerTimeZone.name),g.getTime()>=b.getTime()&&(h=!1,this.scope.startDateError("Start date and time must be in the future"))):(f.startDateError("Provide a valid start date and time"),h=!1),h},this.getRule=function(){var a=this.getOptions();return b(a)},this.getValue=function(){var a=this.getRule(),b=this.getOptions();return{name:f.scheduleName,rrule:a.toString(),options:b}},this.inject=function(a,b){return c({scope:this.scope,target:a,buttons:b})},this.clear=function(){this.clearErrors(),this.scope.scheduler_form.scheduleName.$setPristine(),d(this.scope)}};return new g}}]).factory("Inject",["scheduler_partial","$compile","$http","$log",function(a,b,c,d){return function(e){var f=e.scope,g=e.target,h=e.buttons;f.removeHtmlReady&&f.removeHtmlReady(),f.removeHtmlReady=f.$on("htmlReady",function(a,c){var d=angular.isObject(g)?g:angular.element(document.getElementById(g));d.html(c),b(d)(f),h&&$("#scheduler-buttons").show()}),c({method:"GET",url:a}).success(function(a){f.$emit("htmlReady",a)}).error(function(b,c){d.error("Error calling "+a+". "+c)})}}]).factory("GetRule",["$log",function(a){return function(b){var c,d=b.startDate,e=b.frequency,f=b.interval,g=b.occurrenceCount,h=b.endDate,i=b.month,j=b.monthDay,k=b.weekDays,l=b.setOccurrence,m={};if(angular.isDate(d))m.dtstart=d;else try{m.dtstart=new Date(d)}catch(n){a.error("Date conversion failed. Attempted to convert "+d+" to Date. "+n.message)}if(e&&"none"!==e){if(m.freq=RRule[e.toUpperCase()],m.interval=f,k&&"string"==typeof k&&(m.byweekday=RRule[k.toUpperCase()]),k&&angular.isArray(k))for(m.byweekday=[],c=0;c=0?j.weekDays.splice(a,1):j.weekDays.push(a)},j.startDateError=function(a){j.scheduler_form&&(j.scheduler_form_schedulerStartDt_error=a,j.scheduler_form.schedulerStartDt.$pristine=!1,j.scheduler_form.schedulerStartDt.$dirty=!0,$("#schedulerStartDt").removeClass("ng-pristine").removeClass("ng-valid").removeClass("ng-valid-custom-error").addClass("ng-dirty").addClass("ng-invalid").addClass("ng-invalid-custom-error"))},j.resetStartDate=function(){j.scheduler_form&&(j.scheduler_form_schedulerStartDt_error="",j.scheduler_form.schedulerStartDt.$setValidity("custom-error",!0),j.scheduler_form.schedulerStartDt.$setPristine())},j.removeZonesReady&&j.removeZonesReady(),j.removeZonesReady=j.$on("zonesReady",function(){j.timeZones=JSON.parse(localStorage.zones),j.setDefaults()}),h?c.getZoneList(j):j.setDefaults(),f(j)}}]).factory("CreateObject",["AngularScheduler.useTimezone","$filter","GetRule","Inject","SetDefaults","$timezones","SetRule",function(a,b,c,d,e,f,g){return function(e){var h=function(){this.scope=e,this.useTimezone=a,this.getOptions=function(){var a={};return a.startDate=this.scope.schedulerUTCTime,a.frequency=this.scope.schedulerFrequency.value,a.interval=this.scope.schedulerInterval,"after"===this.scope.schedulerEnd.value&&(a.occurrenceCount=this.scope.schedulerOccurrenceCount),"on"===this.scope.schedulerEnd.value&&(a.endDate=e.schedulerEndDt+this.scope.schedulerUTCTime.replace(/^\d{4}-\d{2}-\d{2}/,"")),"weekly"===this.scope.schedulerFrequency.value?a.weekDays=this.scope.weekDays:"yearly"===this.scope.schedulerFrequency.value?"month"===this.scope.yearlyRepeatOption?(a.month=this.scope.yearlyMonth.value,a.monthDay=this.scope.yearlyMonthDay):(a.setOccurrence=this.scope.yearlyOccurrence.value,a.weekDays=this.scope.yearlyWeekDay.value,a.month=this.scope.yearlyOtherMonth.value):"monthly"===this.scope.schedulerFrequency.value&&("day"===this.scope.monthlyRepeatOption?a.monthDay=this.scope.monthDay:(a.setOccurrence=this.scope.monthlyOccurrence.value,a.weekDays=this.scope.monthlyWeekDay.value)),a},this.clearErrors=function(){this.scope.scheduler_weekDays_error=!1,this.scope.scheduler_endDt_error=!1,this.scope.resetStartDate(),this.scope.scheduler_endDt_error=!1,this.scope.scheduler_form.schedulerEndDt.$setValidity("custom-error",!0),this.scope.scheduler_form.schedulerEndDt.$setPristine(),this.scope.scheduler_form.$setPristine()},this.isValid=function(){var a,c,d,g,h,i,j=!0;if(this.clearErrors(),"weekly"===this.scope.schedulerFrequency.value&&0===e.weekDays.length&&(this.scope.scheduler_weekDays_error=!0,j=!1),this.scope.scheduler_form.schedulerName.$valid||(this.scope.scheduler_form.schedulerName.$dirty=!0,$("#schedulerName").addClass("ng-dirty"),j=!1),"on"===this.scope.schedulerEnd.value&&(/^\d{4}-\d{2}-\d{2}$/.test(this.scope.schedulerEndDt)||(this.scope.scheduler_form.schedulerEndDt.$pristine=!1,this.scope.scheduler_form.schedulerEndDt.$dirty=!0,$("#schedulerEndDt").removeClass("ng-pristine").removeClass("ng-valid").removeClass("ng-valid-custom-error").addClass("ng-dirty").addClass("ng-invalid").addClass("ng-invalid-custom-error"),this.scope.scheduler_endDt_error=!0,j=!1)),this.scope.schedulerUTCTime)try{a=new Date(this.scope.schedulerUTCTime),isNaN(a)?(this.scope.startDateError("Invalid start date and time"),j=!1):(i=a.getTime(),c=new Date,this.useTimezone?(d=c.getFullYear()+"-"+b("schZeroPad")(c.getMonth()+1,2)+"-"+b("schZeroPad")(c.getDate(),2)+"T"+b("schZeroPad")(c.getHours(),2)+":"+b("schZeroPad")(c.getMinutes(),2)+":"+b("schZeroPad")(c.getSeconds(),2)+".000Z",g=f.toUTC(d,this.scope.schedulerTimeZone.name),h=g.getTime()):h=c.getTime(),h>=i&&(this.scope.startDateError("Start date and time must be in the future"),j=!1))}catch(k){this.scope.startDateError("Invalid start date and time"),j=!1}else this.scope.startDateError("Provide a valid start date and time"),j=!1;return j},this.getRRule=function(){var a=this.getOptions();return c(a)},this.getValue=function(){var a=this.getRRule(),b=this.getOptions();return{name:e.schedulerName,rrule:a.toString(),options:b}},this.setRRule=function(a){return this.clear(),g(a,this.scope)},this.setName=function(a){this.scope.schedulerName=a},this.inject=function(a,b){return d({scope:this.scope,target:a,buttons:b})},this.clear=function(){this.clearErrors(),this.scope.scheduler_form.schedulerName.$setPristine(),this.scope.setDefaults()},this.getUserTimezone=function(){return f.getLocal()}};return new h}}]).factory("Inject",["AngularScheduler.partial","$compile","$http","$log",function(a,b,c){return function(d){var e=d.scope,f=d.target,g=d.buttons;e.removeHtmlReady&&e.removeHtmlReady(),e.removeHtmlReady=e.$on("htmlReady",function(a,c){var d=angular.isObject(f)?f:angular.element(document.getElementById(f));d.html(c),b(d)(e),g&&$("#scheduler-buttons").show()}),c({method:"GET",url:a}).success(function(a){e.$emit("htmlReady",a)}).error(function(b,c){throw"Error reading "+a+". "+c})}}]).factory("GetRule",["$log",function(a){return function(b){var c,d=b.startDate,e=b.frequency,f=b.interval,g=b.occurrenceCount,h=b.endDate,i=b.month,j=b.monthDay,k=b.weekDays,l=b.setOccurrence,m={};if(angular.isDate(d))m.dtstart=d;else try{m.dtstart=new Date(d)}catch(n){a.error("Date conversion failed. Attempted to convert "+d+" to Date. "+n.message)}if(e&&"none"!==e){if(m.freq=RRule[e.toUpperCase()],m.interval=f,k&&"string"==typeof k&&(m.byweekday=RRule[k.toUpperCase()]),k&&angular.isArray(k))for(m.byweekday=[],c=0;c 0"),"BYDAY"===r)if("WEEKLY"===g(d,"FREQ"))for(i=s.split(/,/),f.weekDays=[],k=0;k0&&parseInt(s,10)<32?(f.monthDay=parseInt(s,10),f.monhthlyRepeatOption="day"):m="BYMONTHDAY must contain an integer between 1 and 31"),"DTSTART"===r&&(n=!0,/\d{8}T\d{6}Z/.test(s)&&(s=s.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}Z)/,function(a,b,c,d,e,f,g){return b+"-"+c+"-"+d+e+":"+f+":"+g})),a?(l=new Date(s),o=e("schZeroPad")(l.getMonth()+1,2),p=e("schZeroPad")(l.getDate(),2),f.schedulerStartDt=l.getFullYear()+"-"+o+"-"+p,f.schedulerStartHour=e("schZeroPad")(l.getHours(),2),f.schedulerStartMinute=e("schZeroPad")(l.getMinutes(),2),f.schedulerStartSecond=e("schZeroPad")(l.getSeconds(),2),f.scheduleTimeChange()):(q=s.replace(/^.*T/,""),f.schedulerStartDt=s.replace(/T.*$/,""),f.schedulerStartHour=q.substr(0,2),f.schedulerStartMinute=q.substr(3,2),f.schedulerStartMinute=q.substr(6,2)),f.scheduleTimeChange()),"BYSETPOS"===r&&("YEARLY"===g(d,"FREQ")?(f.yearlRepeatOption="other",f.yearlyOccurrence=b.find(f.occurrences,function(a){return a.value===parseInt(s,10)}),f.yearlyOccurrence&&f.yearlyOccurrence.name||(m="BYSETPOS was not in the set of 1,2,3,4,-1")):(f.monthlyOccurrence=b.find(f.occurrences,function(a){return a.value===parseInt(s,10)}),f.monthlyOccurrence&&f.monthlyOccurrence.name||(m="BYSETPOS was not in the set of 1,2,3,4,-1"))),"COUNT"===r&&(parseInt(s,10)?(f.schedulerEnd=f.endOptions[1],f.schedulerOccurrenceCount=parseInt(s,10)):m="COUNT must be a valid integer > 0"),"UNTIL"===r&&(/\d{8}T\d{6}Z/.test(s)&&(s=s.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}Z)/,function(a,b,c,d,e,f,g){return b+"-"+c+"-"+d+e+":"+f+":"+g})),f.schedulerEnd=f.endOptions[2],a?(l=new Date(s),o=e("schZeroPad")(l.getMonth()+1,2),p=e("schZeroPad")(l.getDate(),2),f.schedulerEndDt=l.getFullYear()+"-"+o+"-"+p):f.schedulerEndDt=s.replace(/T.*$/,"")),"BYMONTH"===r&&("YEARLY"===g(d,"FREQ")&&g(d,"BYDAY")?(f.yearlRepeatOption="other",f.yearlyOtherMonth=b.find(f.months,function(a){return a.value===parseInt(s,10)}),f.yearlyOtherMonth&&f.yearlyOtherMonth.name||(m="BYMONTH must be an integer between 1 and 12")):(f.yearlyOption="month",f.yearlyMonth=b.find(f.months,function(a){return a.value===parseInt(s,10)}),f.yearlyMonth&&f.yearlyMonth.name||(m="BYMONTH must be an integer between 1 and 12"))),"BYMONTHDAY"===r&&(parseInt(s,10)?f.yearlyMonthDay=parseInt(s,10):m="BYMONTHDAY must be an integer between 1 and 31")}function j(){"weekly"===f.schedulerFrequency.name&&0===f.weekDays.length&&(m="Frequency is weekly, but BYDAYS value is missing."),n||(m="Warning: start date was not provided")}var k,l,m="",n=!1;if(d)if(k=d.split(/;/),angular.isArray(k)){for(l=0;l" + tab.label + "\n"; + this.form.name + "_tabs')\" href=\"#" + tab.name + "\" data-toggle=\"tab\">" + tab.label + "\n"; } html += "\n"; html += "
\n"; @@ -1396,7 +1395,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) } html += "\">\n"; } - html += "\n"; + html += "Actions\n"; html += "\n"; html += ""; html += "\n"; diff --git a/awx/ui/static/partials/schedule_detail.html b/awx/ui/static/partials/schedule_detail.html new file mode 100644 index 0000000000..1a2f673409 --- /dev/null +++ b/awx/ui/static/partials/schedule_detail.html @@ -0,0 +1,52 @@ +
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+

The scheduler options are invalid or incomplete. Make the needed changes on the options tab, then come back here to see details.

+
+
+
+ + +
+
+ + +
+
+ +
+
Date format
+ +
UTC
+ +
Local time
+
+
    +
  • {{ occurrence.utc }}
  • +
+
    +
  • {{ occurrence.local }}
  • +
+
+
+
+
+
\ No newline at end of file diff --git a/awx/ui/static/sample/data/schedules/data.json b/awx/ui/static/sample/data/schedules/data.json new file mode 100644 index 0000000000..9d95725650 --- /dev/null +++ b/awx/ui/static/sample/data/schedules/data.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/awx/ui/static/sample/data/schedules/projects/data.json b/awx/ui/static/sample/data/schedules/projects/data.json new file mode 100644 index 0000000000..c9c7031ab2 --- /dev/null +++ b/awx/ui/static/sample/data/schedules/projects/data.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/awx/ui/static/scripts/web-server.js b/awx/ui/static/scripts/web-server.js new file mode 100755 index 0000000000..3f74441e31 --- /dev/null +++ b/awx/ui/static/scripts/web-server.js @@ -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('<', '<'). + replace('>', '>'). + replace('"', '"'); +} + +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('\n'); + res.write('Internal Server Error\n'); + res.write('

Internal Server Error

'); + res.write('
' + escapeHtml(util.inspect(error)) + '
'); + 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('\n'); + res.write('404 Not Found\n'); + res.write('

Not Found

'); + res.write( + '

The requested URL ' + + escapeHtml(path) + + ' was not found on this server.

' + ); + 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('\n'); + res.write('403 Forbidden\n'); + res.write('

Forbidden

'); + res.write( + '

You do not have permission to access ' + + escapeHtml(path) + ' on this server.

' + ); + 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('\n'); + res.write('301 Moved Permanently\n'); + res.write('

Moved Permanently

'); + res.write( + '

The document has moved here.

' + ); + 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('\n'); + res.write('' + escapeHtml(path) + '\n'); + res.write('\n'); + res.write('

Directory: ' + escapeHtml(path) + '

'); + res.write('
    '); + files.forEach(function(fileName) { + if (fileName.charAt(0) !== '.') { + res.write('
  1. ' + + escapeHtml(fileName) + '
  2. '); + } + }); + res.write('
'); + res.end(); +}; + +// Must be last, +main(process.argv); diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index b1498e1e52..73301cab58 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -33,6 +33,16 @@ + + + + + + + + + + {% if settings.USE_MINIFIED_JS %} {% else %} @@ -66,6 +76,7 @@ + @@ -104,6 +115,7 @@ + @@ -128,6 +140,7 @@ + @@ -401,7 +414,7 @@ - +