mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Incorporated latest angular-scheduler changes. Starting to add 'detail' schedule view to the inventory group edit dialog, allowing user to see occurrences and natural language description of a given schedule object.
This commit is contained in:
parent
0587583f19
commit
85b8b12e51
@ -93,7 +93,7 @@ angular.module('ansible', [
|
||||
'SchedulesHelper'
|
||||
])
|
||||
|
||||
.constant('AngularScheduler.partial', $basePath + 'lib/angular-scheduler/lib/angular-scheduler.html')
|
||||
.constant('AngularScheduler.partials', $basePath + 'lib/angular-scheduler/lib/')
|
||||
.constant('AngularScheduler.useTimezone', false)
|
||||
.constant('AngularScheduler.showUTCField', false)
|
||||
.constant('$timezones.definitions.location', $basePath + 'lib/angular-tz-extensions/tz/data')
|
||||
|
||||
@ -705,7 +705,6 @@ function(SchedulerInit, Rest, Wait) {
|
||||
}])
|
||||
|
||||
|
||||
|
||||
.factory('GroupsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm',
|
||||
'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate',
|
||||
'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find','WatchInventoryWindowResize',
|
||||
|
||||
@ -76,9 +76,6 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
}
|
||||
});
|
||||
$('#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%
|
||||
@ -102,12 +99,16 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
open: function () {
|
||||
Wait('stop');
|
||||
$('#schedulerName').focus();
|
||||
$('#rrule_nlp_description').dblclick(function() {
|
||||
console.log('here!');
|
||||
setTimeout(function() { scope.$apply(function() { scope.showRRule = (scope.showRRule) ? false : true; }); }, 100);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('ShowDetails', [ function() {
|
||||
/*.factory('ShowDetails', [ function() {
|
||||
return function(params) {
|
||||
|
||||
var scope = params.scope,
|
||||
@ -137,10 +138,10 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
}
|
||||
}
|
||||
};
|
||||
}])
|
||||
}])*/
|
||||
|
||||
.factory('EditSchedule', ['SchedulerInit', 'ShowSchedulerModal', 'ShowDetails', 'Wait', 'Rest',
|
||||
function(SchedulerInit, ShowSchedulerModal, ShowDetails, Wait, Rest) {
|
||||
.factory('EditSchedule', ['SchedulerInit', 'ShowSchedulerModal', 'Wait', 'Rest',
|
||||
function(SchedulerInit, ShowSchedulerModal, Wait, Rest) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
schedule = params.schedule,
|
||||
@ -149,8 +150,10 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
|
||||
Wait('start');
|
||||
$('#form-container').empty();
|
||||
scheduler = SchedulerInit({ scope: scope });
|
||||
scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false });
|
||||
scheduler.inject('form-container', false);
|
||||
scheduler.injectDetail('occurrences', false);
|
||||
|
||||
ShowSchedulerModal({ scope: scope });
|
||||
scope.showRRuleDetail = false;
|
||||
|
||||
@ -167,7 +170,6 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
var newSchedule;
|
||||
$('#scheduler-tabs a:first').tab('show');
|
||||
if (scheduler.isValid()) {
|
||||
scope.schedulerIsValid = true;
|
||||
Wait('start');
|
||||
newSchedule = scheduler.getValue();
|
||||
schedule.name = newSchedule.name;
|
||||
@ -183,19 +185,20 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
$('#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 });
|
||||
if ($(e.target).text() === 'Details') {
|
||||
if (!scheduler.isValid()) {
|
||||
$('#scheduler-tabs a:first').tab('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('AddSchedule', ['SchedulerInit', 'ShowSchedulerModal', 'ShowDetails', 'Wait', 'Rest',
|
||||
function(SchedulerInit, ShowSchedulerModal, ShowDetails, Wait, Rest) {
|
||||
.factory('AddSchedule', ['SchedulerInit', 'ShowSchedulerModal', 'Wait', 'Rest',
|
||||
function(SchedulerInit, ShowSchedulerModal, Wait, Rest) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
url = params.url,
|
||||
@ -204,8 +207,9 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
|
||||
Wait('start');
|
||||
$('#form-container').empty();
|
||||
scheduler = SchedulerInit({ scope: scope });
|
||||
scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false });
|
||||
scheduler.inject('form-container', false);
|
||||
scheduler.injectDetail('occurrences', false);
|
||||
scheduler.clear();
|
||||
ShowSchedulerModal({ scope: scope });
|
||||
scope.showRRuleDetail = false;
|
||||
@ -218,7 +222,6 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
var newSchedule;
|
||||
$('#scheduler-tabs a:first').tab('show');
|
||||
if (scheduler.isValid()) {
|
||||
scope.schedulerIsValid = true;
|
||||
Wait('start');
|
||||
newSchedule = scheduler.getValue();
|
||||
schedule.name = newSchedule.name;
|
||||
@ -234,13 +237,14 @@ angular.module('SchedulesHelper', ['Utilities', 'SchedulesHelper'])
|
||||
$('#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 });
|
||||
if ($(e.target).text() === 'Details') {
|
||||
if (!scheduler.isValid()) {
|
||||
$('#scheduler-tabs a:first').tab('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
@ -37,6 +37,10 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#schedules-detail {
|
||||
display:none;
|
||||
}
|
||||
|
||||
#scheduler-modal-dialog, #schedules-form-container {
|
||||
display: none;
|
||||
overflow-x: hidden;
|
||||
@ -55,6 +59,15 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.occurrence-list {
|
||||
border: 1px solid @well-border;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
background-color: @well;
|
||||
list-style: none;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#date-choice {
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
|
||||
2
awx/ui/static/lib/angular-scheduler/.gitignore
vendored
Normal file
2
awx/ui/static/lib/angular-scheduler/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bower_components
|
||||
node_modules
|
||||
21
awx/ui/static/lib/angular-scheduler/.jshintrc
Normal file
21
awx/ui/static/lib/angular-scheduler/.jshintrc
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
// Details: https://github.com/victorporof/Sublime-JSHint#using-your-own-jshintrc-options
|
||||
// Example: https://github.com/jshint/jshint/blob/master/examples/.jshintrc
|
||||
// Documentation: http://www.jshint.com/docs/
|
||||
|
||||
"browser": true,
|
||||
"jquery": true,
|
||||
"esnext": true,
|
||||
"globalstrict": true,
|
||||
"globals": { "angular":false, "alert":true },
|
||||
"strict": false,
|
||||
"quotmark": false,
|
||||
"smarttabs": true,
|
||||
"trailing": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"eqeqeq": true,
|
||||
"indent": 4,
|
||||
"onevar": true,
|
||||
"newcap": false
|
||||
}
|
||||
@ -15,13 +15,60 @@ Using
|
||||
Coming soon...
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
Sample App
|
||||
----------
|
||||
|
||||
An example site is included along with a simple node based web server. With [node](http://nodejs.org) installed, run the following:
|
||||
An example application is included along with a simple node based web server. With [node](http://nodejs.org) installed, run the following to start the server:
|
||||
|
||||
node scripts/web-server.js
|
||||
node ./scripts/web-server.js 8000
|
||||
|
||||
Once that's running, point you browser to http://localhost:8000/app/index.html
|
||||
Visit the sample by pointing your browser to http://localhost:8000/app/index.html
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
After cloning the repo, install the the bower packages listed in bower.json:
|
||||
|
||||
bower install
|
||||
|
||||
Install the npm packages listed in package.json:
|
||||
|
||||
node install
|
||||
|
||||
Install [Grunt](http://www.gruntjs.com) command line:
|
||||
|
||||
npm install -g grunt-cli
|
||||
|
||||
From the project root run the grunt command. This will execute the default steps found in Gruntfile.js, which will lint and minify the javascript and css files:
|
||||
|
||||
grunt
|
||||
|
||||
You should see output similar to the following:
|
||||
|
||||
Running "jshint:uses_defaults" (jshint) task
|
||||
>> 2 files lint free.
|
||||
|
||||
Running "uglify:my_target" (uglify) task
|
||||
File "lib/angular-scheduler.min.js" created.
|
||||
|
||||
Running "less:production" (less) task
|
||||
File lib/angular-scheduler.min.css created.
|
||||
|
||||
Run tests found in the ./tests directory. GetRRule.js provides a set of unit tests. Install [Karma](http://karma-runner.github.io/0.12/index.html), and launch with the folllowing:
|
||||
|
||||
cd test
|
||||
karma start
|
||||
|
||||
SetRRule.js provides end-to-end tests that run with [Protractor](https://github.com/angular/protractor). Follow the instructions to install protractor and a local selenium server (assuming you don't have access to an existing selenium server). Launch the provided sample app (as described above) in a terminal session. In a separate terminal session launch a local selenium server. The test configuration file expects the web server to run at localhost:8000 and the selenium server to run at localhost:4444. In a third session luanch the tests:
|
||||
|
||||
Session 1:
|
||||
node ./scripts/web-server.js 8000
|
||||
|
||||
Session 2:
|
||||
webdriver-manager start
|
||||
|
||||
Session 3:
|
||||
cd tests
|
||||
protractor protractorConf.js
|
||||
|
||||
|
||||
|
||||
@ -43,6 +43,10 @@ body {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#scope-variables {
|
||||
margin-top: 65px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
@ -196,4 +200,18 @@ a:hover {
|
||||
}
|
||||
.ui-front {
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
#message {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
#scope-variables {
|
||||
margin-left: 25px;
|
||||
}
|
||||
}
|
||||
@ -20,16 +20,33 @@ angular.module('sampleApp', ['ngRoute', 'AngularScheduler', 'Timezones'])
|
||||
});
|
||||
}])
|
||||
|
||||
.constant('AngularScheduler.partial', '/lib/angular-scheduler.html')
|
||||
.run(['$rootScope', function($rootScope) {
|
||||
$rootScope.toggleTab = function(e, tab, tabs) {
|
||||
e.preventDefault();
|
||||
$('#' + tabs + ' #' + tab).tab('show');
|
||||
};
|
||||
}])
|
||||
|
||||
.constant('AngularScheduler.partial', '/lib/')
|
||||
.constant('AngularScheduler.useTimezone', false)
|
||||
.constant('AngularScheduler.showUTCField', false)
|
||||
.constant('$timezones.definitions.location', '/bower_components/angular-tz-extensions/tz/data')
|
||||
|
||||
.controller('sampleController', ['$scope', '$filter', 'SchedulerInit', function($scope, $filter, SchedulerInit) {
|
||||
|
||||
var scheduler = SchedulerInit({ scope: $scope });
|
||||
var scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false });
|
||||
|
||||
scheduler.inject('form-container', true);
|
||||
scheduler.injectDetail('details-tab', true);
|
||||
|
||||
$('#scheduler-tabs a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
|
||||
// Only show detail tab if schedule is valid
|
||||
if ($(e.target).text() === 'Details') {
|
||||
if (!scheduler.isValid()) {
|
||||
$('#scheduler-link').tab('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.setRRule = function() {
|
||||
$scope.inputRRuleMsg = '';
|
||||
@ -41,62 +58,32 @@ angular.module('sampleApp', ['ngRoute', 'AngularScheduler', 'Timezones'])
|
||||
$scope.saveForm = function() {
|
||||
if (scheduler.isValid()) {
|
||||
var schedule = scheduler.getValue(),
|
||||
rrule = scheduler.getRRule(),
|
||||
html,
|
||||
wheight = $(window).height(),
|
||||
wwidth = $(window).width(),
|
||||
w, h, occurrences;
|
||||
w, h;
|
||||
|
||||
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";
|
||||
$('#message').empty();
|
||||
scheduler.injectDetail('message', true);
|
||||
|
||||
w = (600 > wwidth) ? wwidth : 600;
|
||||
h = (600 > wheight) ? wheight : 600;
|
||||
h = (635 > wheight) ? wheight : 635;
|
||||
|
||||
$('#message').html(html)
|
||||
.dialog({
|
||||
title: schedule.name,
|
||||
modal: true,
|
||||
width: w,
|
||||
height: h,
|
||||
position: 'center',
|
||||
buttons: { OK: function() { $(this).dialog('close');} },
|
||||
open: function () {
|
||||
// fix the close button
|
||||
$('.ui-dialog[aria-describedby="message"]').find('.ui-dialog-titlebar button')
|
||||
.empty().attr({ 'class': 'close' }).text('x');
|
||||
// fix the OK button
|
||||
$('.ui-dialog[aria-describedby="message"]').find('.ui-dialog-buttonset button:first')
|
||||
.attr({ 'class': 'btn btn-primary', 'id': 'modal-ok-button' });
|
||||
}
|
||||
});
|
||||
$('#message').dialog({
|
||||
title: schedule.name,
|
||||
modal: true,
|
||||
width: w,
|
||||
height: h,
|
||||
position: 'center',
|
||||
buttons: { OK: function() { $(this).dialog('close');} },
|
||||
open: function () {
|
||||
// fix the close button
|
||||
$('.ui-dialog[aria-describedby="message"]').find('.ui-dialog-titlebar button')
|
||||
.empty().attr({ 'class': 'close' }).text('x');
|
||||
// fix the OK button
|
||||
$('.ui-dialog[aria-describedby="message"]').find('.ui-dialog-buttonset button:first')
|
||||
.attr({ 'class': 'btn btn-primary', 'id': 'modal-ok-button' });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="well">
|
||||
<div id="form-container"></div>
|
||||
|
||||
<ul id="scheduler-tabs" class="nav nav-tabs">
|
||||
<li class="active"><a id="scheduler-link" href="#scheduler-tab" data-toggle="tab"
|
||||
ng-click="toggleTab($event, 'scheduler-link', 'scheduler-tabs')">Scheduler</a></li>
|
||||
<li><a id="details-link" href="#details-tab" data-toggle="tab"
|
||||
ng-click="toggleTab($event, 'details-link', 'scheduler-tabs')">Details</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="scheduler-tab">
|
||||
<div id="form-container"></div>
|
||||
</div>
|
||||
<div class="tab-pane" id="details-tab"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<hr class="visible-sm visible-xs"/>
|
||||
<table class="table table-condensed">
|
||||
<table class="table table-condensed" id="scope-variables">
|
||||
<theader>
|
||||
<tr>
|
||||
<th>Scope Variable</th>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-scheduler",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"authors": [
|
||||
"Chris Houseknecht <chouse@ansible.com>"
|
||||
],
|
||||
@ -24,10 +24,10 @@
|
||||
"dependencies": {
|
||||
"angular": "~1.2.14",
|
||||
"underscore": "*",
|
||||
"twitter": "*",
|
||||
"bootstrap": "~3.1.1",
|
||||
"rrule": "*",
|
||||
"timezone-js": "*",
|
||||
"angular-tz-extensions":"*"
|
||||
"angular-tz-extensions": "*"
|
||||
},
|
||||
"homepage": "https://github.com/chouseknecht/angular-scheduler",
|
||||
"main": "git@github.com:chouseknecht/angular-scheduler.git",
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
<!--
|
||||
angular-scheduler-detail.html
|
||||
|
||||
Partial to be injected on inectDetail() method call, providing occurrence list, rrule and a natural language description.
|
||||
|
||||
Copyright (c) 2014 Ansible, Inc.
|
||||
|
||||
Maintainers:
|
||||
Chris Houseknecht
|
||||
@chouseknecht
|
||||
chouse@ansible.com
|
||||
-->
|
||||
<div id="scheduler-detail">
|
||||
<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="showRRule">
|
||||
<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>
|
||||
@ -91,4 +91,46 @@ input.ng-dirty.ng-invalid, select.ng-dirty.ng-invalid, textarea.ng-dirty.ng-inva
|
||||
}
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Details page */
|
||||
|
||||
#occurrence-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.occurrence-list {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
list-style: none;
|
||||
margin-bottom: 5px;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
#date-choice {
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#date-choice .label-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#date-choice input {
|
||||
margin-bottom: 2px;
|
||||
height: 11px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
#date-choice .label-inline:first-child {
|
||||
padding-bottom: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#date-choice .label-inline:nth-child(3) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@ -24,7 +24,7 @@ angular.module('underscore',[])
|
||||
|
||||
angular.module('AngularScheduler', ['underscore'])
|
||||
|
||||
.constant('AngularScheduler.partial', '/lib/angular-scheduler.html')
|
||||
.constant('AngularScheduler.partials', '/lib/')
|
||||
.constant('AngularScheduler.useTimezone', false)
|
||||
.constant('AngularScheduler.showUTCField', false)
|
||||
|
||||
@ -178,8 +178,8 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
user input is valid, reset the form, etc. All the things we need to access and manipulate the
|
||||
scheduler widget
|
||||
*/
|
||||
.factory('CreateObject', ['AngularScheduler.useTimezone', '$filter', 'GetRule', 'Inject', 'SetDefaults', '$timezones', 'SetRule',
|
||||
function(useTimezone, $filter, GetRule, Inject, SetDefaults, $timezones, SetRule) {
|
||||
.factory('CreateObject', ['AngularScheduler.useTimezone', '$filter', 'GetRule', 'Inject', 'InjectDetail', 'SetDefaults', '$timezones', 'SetRule',
|
||||
function(useTimezone, $filter, GetRule, Inject, InjectDetail, SetDefaults, $timezones, SetRule) {
|
||||
return function(scope, requireFutureST) {
|
||||
var fn = function() {
|
||||
|
||||
@ -238,6 +238,25 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
}
|
||||
};
|
||||
|
||||
// Set values for detail page
|
||||
this.setDetails = function() {
|
||||
var rrule = this.getRRule();
|
||||
if (rrule) {
|
||||
scope.rrule_nlp_description = rrule.toText();
|
||||
scope.dateChoice = 'utc';
|
||||
scope.occurrence_list = [];
|
||||
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 = rrule.toString();
|
||||
}
|
||||
};
|
||||
|
||||
// Check the input form for errors
|
||||
this.isValid = function() {
|
||||
var startDt, now, dateStr, adjNow, timeNow, timeFuture, validity = true;
|
||||
@ -301,6 +320,12 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
this.scope.startDateError("Provide a start time");
|
||||
validity = false;
|
||||
}
|
||||
|
||||
scope.schedulerIsValid = validity;
|
||||
if (validity) {
|
||||
this.setDetails();
|
||||
}
|
||||
|
||||
return validity;
|
||||
};
|
||||
|
||||
@ -338,6 +363,10 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
return Inject({ scope: this.scope, target: element, buttons: showButtons });
|
||||
};
|
||||
|
||||
this.injectDetail = function(element, showRRule) {
|
||||
return InjectDetail({ scope: this.scope, target: element, showRRule: showRRule });
|
||||
};
|
||||
|
||||
// Clear the form, returning all elements to a default state
|
||||
this.clear = function() {
|
||||
this.clearErrors();
|
||||
@ -360,12 +389,16 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
this.getRequireFutureStartTime = function() {
|
||||
return this.requireFutureStartTime;
|
||||
};
|
||||
|
||||
this.setShowRRule = function(opt) {
|
||||
scope.showRRule = opt;
|
||||
};
|
||||
};
|
||||
return new fn();
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('Inject', ['AngularScheduler.partial', '$compile', '$http', '$log', function(scheduler_partial, $compile, $http) {
|
||||
.factory('Inject', ['AngularScheduler.partials', '$compile', '$http', '$log', function(scheduler_partial, $compile, $http) {
|
||||
return function(params) {
|
||||
|
||||
var scope = params.scope,
|
||||
@ -384,12 +417,41 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
}
|
||||
});
|
||||
|
||||
$http({ method: 'GET', url: scheduler_partial })
|
||||
$http({ method: 'GET', url: scheduler_partial + 'angular-scheduler.html' })
|
||||
.success( function(data) {
|
||||
scope.$emit('htmlReady', data);
|
||||
})
|
||||
.error( function(data, status) {
|
||||
throw('Error reading ' + scheduler_partial + '. ' + status);
|
||||
throw('Error reading ' + scheduler_partial + 'angular-scheduler.html. ' + status);
|
||||
//$log.error('Error calling ' + scheduler_partial + '. ' + status);
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('InjectDetail', ['AngularScheduler.partials', '$compile', '$http', '$log', function(scheduler_partial, $compile, $http) {
|
||||
return function(params) {
|
||||
|
||||
var scope = params.scope,
|
||||
target = params.target,
|
||||
showRRule = params.showRRule;
|
||||
|
||||
scope.showRRule = showRRule || false;
|
||||
|
||||
if (scope.removeHtmlDetailReady) {
|
||||
scope.removeHtmlDetailReady();
|
||||
}
|
||||
scope.removeHtmlDetailReady = scope.$on('htmlDetailReady', function(e, data) {
|
||||
var element = (angular.isObject(target)) ? target : angular.element(document.getElementById(target));
|
||||
element.html(data);
|
||||
$compile(element)(scope);
|
||||
});
|
||||
|
||||
$http({ method: 'GET', url: scheduler_partial + 'angular-scheduler-detail.html' })
|
||||
.success( function(data) {
|
||||
scope.$emit('htmlDetailReady', data);
|
||||
})
|
||||
.error( function(data, status) {
|
||||
throw('Error reading ' + scheduler_partial + 'angular-scheduler-detail.html. ' + status);
|
||||
//$log.error('Error calling ' + scheduler_partial + '. ' + status);
|
||||
});
|
||||
};
|
||||
@ -756,6 +818,13 @@ angular.module('AngularScheduler', ['underscore'])
|
||||
scope.weekDayFRClass = '';
|
||||
scope.weekDaySAClass = '';
|
||||
scope.weekDaySUClass = '';
|
||||
|
||||
//Detail view
|
||||
scope.schedulerIsValid = false;
|
||||
scope.rrule_nlp_description = '';
|
||||
scope.rrule = '';
|
||||
scope.dateChoice = 'utc';
|
||||
scope.occurrence_list = [];
|
||||
};
|
||||
}])
|
||||
|
||||
|
||||
@ -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}.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%}
|
||||
.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%}#occurrence-label{display:inline-block}.occurrence-list{border:1px solid #ccc;padding:8px 10px;border-radius:4px;background-color:#eee;list-style:none;margin-bottom:5px;min-height:220px}#date-choice{display:inline-block;margin-left:15px;font-size:12px}#date-choice .label-inline{display:inline-block;vertical-align:middle}#date-choice input{margin-bottom:2px;height:11px;width:10px}#date-choice .label-inline:first-child{padding-bottom:2px;margin-right:10px}#date-choice .label-inline:nth-child(3){margin-right:10px}
|
||||
File diff suppressed because one or more lines are too long
195
awx/ui/static/lib/angular-scheduler/tests/GetRRule.js
vendored
Normal file
195
awx/ui/static/lib/angular-scheduler/tests/GetRRule.js
vendored
Normal file
@ -0,0 +1,195 @@
|
||||
/* GetRRule.js
|
||||
*
|
||||
* Unit tests for scheduler widget
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
describe("Get RRule without timezone", function() {
|
||||
|
||||
var SchedulerInit,
|
||||
$scope,
|
||||
schedules;
|
||||
|
||||
schedules = [{
|
||||
schedulerStartDt: '2013-12-12',
|
||||
schedulerStartHour: '13',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerFrequency: { name: 'Daily', value: 'daily' },
|
||||
schedulerInterval: 3,
|
||||
schedulerEnd: { name: 'On Day', value: 'on' },
|
||||
schedulerEndDt: '2014-03-28',
|
||||
result: "FREQ=DAILY;DTSTART=20131212T130000Z;INTERVAL=3;UNTIL=20140328T130000Z"
|
||||
},
|
||||
{
|
||||
schedulerStartDt: '2014-03-03',
|
||||
schedulerStartHour: '17',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerFrequency: { name: 'Weekly', value: 'weekly' },
|
||||
schedulerInterval: 1,
|
||||
weekDays: ["su","mo","sa"],
|
||||
schedulerEnd: { name: 'After', value: 'after' },
|
||||
schedulerOccurrenceCount: 5,
|
||||
result: "FREQ=WEEKLY;DTSTART=20140303T170000Z;INTERVAL=1;COUNT=5;BYDAY=SU,MO,SA"
|
||||
},
|
||||
{
|
||||
schedulerStartDt: '2014-03-13',
|
||||
schedulerStartHour: '00',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerFrequency: { name: 'Monthly', value: 'monthly' },
|
||||
schedulerInterval: 1,
|
||||
monthlyRepeatOption: 'day',
|
||||
monthDay: 1,
|
||||
schedulerEnd: { name: 'Never', value: 'never' },
|
||||
result: "FREQ=MONTHLY;DTSTART=20140313T000000Z;INTERVAL=1;BYMONTHDAY=1"
|
||||
},
|
||||
{
|
||||
schedulerStartDt: '2014-03-13',
|
||||
schedulerStartHour: '00',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerFrequency: { name: 'Monthly', value: 'monthly' },
|
||||
schedulerInterval: 1,
|
||||
monthlyRepeatOption: 'other',
|
||||
monthlyOccurrence: { name: 'third', value: 3 },
|
||||
monthlyWeekDay: { name: 'Weekend day', value: ["sa","su"] },
|
||||
schedulerEnd: { name: 'Never', value: 'never' },
|
||||
result: "FREQ=MONTHLY;DTSTART=20140313T000000Z;INTERVAL=1;BYSETPOS=3;BYDAY=SA,SU"
|
||||
},
|
||||
{
|
||||
schedulerStartDt: '2014-03-19',
|
||||
schedulerStartHour: '00',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerFrequency: { name: 'Yearly', value: 'yearly' },
|
||||
schedulerInterval: 5,
|
||||
yearlyRepeatOption: 'month',
|
||||
yearlyMonth: { name: 'April', value: 4 },
|
||||
yearlyMonthDay: 1,
|
||||
schedulerEnd: { name: 'Never', value: 'never' },
|
||||
result: "FREQ=YEARLY;DTSTART=20140319T000000Z;INTERVAL=5;BYMONTH=4;BYMONTHDAY=1"
|
||||
},
|
||||
{
|
||||
schedulerStartDt: '2014-03-19',
|
||||
schedulerStartHour: '00',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerFrequency: { name: 'Yearly', value: 'yearly' },
|
||||
schedulerInterval: 1,
|
||||
yearlyRepeatOption: 'other',
|
||||
yearlyOccurrence: { name: 'last', value: -1 },
|
||||
yearlyWeekDay: { name: 'Monday', value: 'mo' },
|
||||
yearlyOtherMonth: { name: 'July', value: 7 },
|
||||
schedulerEnd: { name: 'After', value: 'after' },
|
||||
schedulerOccurrenceCount: 5,
|
||||
result: "FREQ=YEARLY;DTSTART=20140319T000000Z;INTERVAL=1;COUNT=5;BYSETPOS=-1;BYMONTH=7;BYDAY=MO"
|
||||
}];
|
||||
|
||||
beforeEach(function() {
|
||||
module('Timezones', function($provide) {
|
||||
$provide.constant('$timezones.definitions.location', '/base/bower_components/angular-tz-extensions/tz/data');
|
||||
});
|
||||
module('AngularScheduler', function($provide) {
|
||||
$provide.constant('AngularScheduler.partials', '/lib/');
|
||||
});
|
||||
inject( function($rootScope, _SchedulerInit_) {
|
||||
SchedulerInit = _SchedulerInit_;
|
||||
$scope = $rootScope.$new(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
it('should return an object', function() {
|
||||
var scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false });
|
||||
expect(scheduler.inject).toBeDefined();
|
||||
});
|
||||
|
||||
schedules.forEach(function(sched, idx) {
|
||||
it('should return ' + sched.result, function() {
|
||||
var scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false}),
|
||||
result, key;
|
||||
for(key in sched) {
|
||||
$scope[key] = sched[key];
|
||||
}
|
||||
$scope.scheduleTimeChange();
|
||||
result = scheduler.getValue(),
|
||||
//console.log('Test ' + idx + ' ' + sched.result);
|
||||
expect(result.rrule).toEqual(sched.result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* Make sure timezone conversion works. Will have to adjust later for DST */
|
||||
describe("Get RRule with timezone", function() {
|
||||
|
||||
var SchedulerInit,
|
||||
$scope,
|
||||
schedules,
|
||||
scheduler;
|
||||
|
||||
schedules = [{
|
||||
schedulerStartDt: '2014-03-01',
|
||||
schedulerStartHour: '18',
|
||||
schedulerStartMinute: '00',
|
||||
schedulerStartSecond: '00',
|
||||
schedulerTimeZone: { name: 'America/New_York' },
|
||||
schedulerFrequency: { name: 'Daily', value: 'daily' },
|
||||
schedulerInterval: 1,
|
||||
schedulerEnd: { name: 'Never', value: 'never' },
|
||||
result: "FREQ=DAILY;DTSTART=20140301T230000Z;INTERVAL=1"
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(function() {
|
||||
module('Timezones', function($provide) {
|
||||
$provide.constant('$timezones.definitions.location', '/base/bower_components/angular-tz-extensions/tz/data');
|
||||
});
|
||||
module('AngularScheduler', function($provide) {
|
||||
$provide.constant('AngularScheduler.useTimezone',true);
|
||||
$provide.constant('AngularScheduler.partials', '/lib/');
|
||||
});
|
||||
inject( function($rootScope, _SchedulerInit_) {
|
||||
SchedulerInit = _SchedulerInit_;
|
||||
$scope = $rootScope;
|
||||
//.$new(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
it('should return an object', function() {
|
||||
var scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false });
|
||||
expect(scheduler.inject).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get the local timezone', function() {
|
||||
var scheduler = SchedulerInit({ scope: $scope }),
|
||||
user_timezone = scheduler.getUserTimezone();
|
||||
expect(user_timezone).toBeDefined();
|
||||
});
|
||||
|
||||
schedules.forEach(function(sched, idx) {
|
||||
it('should return ' + sched.result, function() {
|
||||
var scheduler = SchedulerInit({ scope: $scope, requireFutureStartTime: false }),
|
||||
result, key;
|
||||
for(key in sched) {
|
||||
$scope[key] = sched[key];
|
||||
}
|
||||
$scope.scheduleTimeChange();
|
||||
result = scheduler.getValue(),
|
||||
//console.log('Test ' + idx + ' ' + sched.result);
|
||||
expect(result.rrule).toEqual(sched.result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
85
awx/ui/static/lib/angular-scheduler/tests/SetRRule.js
vendored
Normal file
85
awx/ui/static/lib/angular-scheduler/tests/SetRRule.js
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
Run this script with protractor against the sample app. It assumes the timezone option,
|
||||
AngularScheduler.useTimezone, is set to false.
|
||||
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
describe("Scheduler Widget", function() {
|
||||
|
||||
it('should return the expected RRule', function() {
|
||||
var now = new Date(),
|
||||
tomorrow = new Date( now.getTime() + (24 * 60 * 60 * 1000) ),
|
||||
sendDateString = tomorrow.getFullYear() + '-' + ('00' + (tomorrow.getMonth() + 1)).substr(-2,2) +
|
||||
'-' + ('00' + tomorrow.getDate()).substr(-2,2),
|
||||
receiveDateString = sendDateString.replace(/-/g,'') + 'T000000Z',
|
||||
result;
|
||||
browser.get('/app/index.html');
|
||||
element(by.model('schedulerName')).sendKeys('Schedule 1');
|
||||
element(by.model('schedulerStartDt')).clear();
|
||||
element(by.model('schedulerStartDt')).sendKeys(sendDateString);
|
||||
element(by.model('schedulerStartDt')).sendKeys(protractor.Key.ESCAPE);
|
||||
|
||||
element(by.css('#schedulerFrequency option:nth-child(4)')).click()
|
||||
.then(function() {
|
||||
var interval = element(by.id('schedulerInterval'));
|
||||
interval.clear();
|
||||
interval.sendKeys('3');
|
||||
element(by.css('#scheduler-buttons #save-button')).click();
|
||||
browser.sleep(3000)
|
||||
.then(function() {
|
||||
element(by.css('#scheduler-detail #rrule')).getAttribute('value')
|
||||
.then(function(txt) {
|
||||
element(by.id('modal-ok-button')).click();
|
||||
expect(txt).toEqual('FREQ=DAILY;DTSTART=' + receiveDateString + ';INTERVAL=3');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//NOTE: getText() on ng-model input returns an empty string. Resorted to getting value of attribute 'value'
|
||||
|
||||
it('should translate given RRule', function() {
|
||||
var rrule = 'FREQ=WEEKLY;DTSTART=20140313T000000Z;INTERVAL=1;COUNT=1;BYDAY=MO,FR';
|
||||
|
||||
browser.get('/app/index.html');
|
||||
element(by.model('inputRRule')).sendKeys(rrule);
|
||||
element(by.id('show-me-button')).click().then(function() {
|
||||
|
||||
browser.sleep(1000).then(function() {
|
||||
var formDate, formHour, formMinute, formSecond,
|
||||
formFrequencySelected, formInterval, formSchedulerEndSelected, weekDays=[], formCount;
|
||||
|
||||
function checkDay(day) {
|
||||
element(by.css('button[data-value="' + day.toUpperCase() + '"]')).getAttribute('class')
|
||||
.then(function(cls){ console.log('check weekDay ' + day); expect(/active/.test(cls)).toBeTruthy(); },
|
||||
function(e) { console.log('check weekDay ' + day); console.log('Failed: ' + e); }
|
||||
);
|
||||
}
|
||||
|
||||
element(by.css('#schedulerFrequency option:nth-child(5)')).isSelected()
|
||||
.then(function(selected) { console.log('schedulerFrequency'); expect(selected).toBeTruthy(); },
|
||||
function(e) { console.log('Failed: ' + e); });
|
||||
|
||||
element(by.model('schedulerStartDt')).getAttribute('value')
|
||||
.then(function(txt) { console.log('schedulerStartDt'); expect(txt).toEqual('2014-03-13'); },
|
||||
function(e) { console.log('schedulerStartDt'); console.log('Failed: ' + e); });
|
||||
|
||||
element(by.model('schedulerInterval')).getAttribute('value')
|
||||
.then(function(txt){ console.log('schedulerInterval'); expect(txt).toEqual('1'); },
|
||||
function(e) { console.log('schedulerInterval'); console.log('Failed: ' + e); });
|
||||
|
||||
checkDay('MO');
|
||||
checkDay('FR');
|
||||
|
||||
element(by.css('#schedulerEnd option:nth-child(2)')).isSelected()
|
||||
.then(function(selected) { console.log('schedulerEnd'); expect(selected).toBeTruthy(); },
|
||||
function(e) { console.log('schedulerEnd'); console.log('Failed: ' + e); });
|
||||
|
||||
//element(by.id('schedulerOccurrenceCount')).getText().then(function(txt){ formCount = txt; });
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
40
awx/ui/static/lib/angular-scheduler/tests/index.html
Normal file
40
awx/ui/static/lib/angular-scheduler/tests/index.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Jasmine Spec Runner v2.0.0</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.0.0/jasmine_favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/jasmine.css">
|
||||
|
||||
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
|
||||
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
|
||||
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
|
||||
|
||||
<!-- include source files here... -->
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script src="/bower_components/jqueryui/ui/minified/jquery-ui.min.js"></script>
|
||||
<script src="/bower_components/twitter/dist/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- timezone -->
|
||||
<script src="/bower_components/timezone-js/src/date.js"></script>
|
||||
<script src="/bower_components/angular-tz-extensions/packages/jstimezonedetect/jstz.min.js"></script>
|
||||
<!-- rrule -->
|
||||
<script src="/bower_components/underscore/underscore.js"></script>
|
||||
<script src="/bower_components/rrule/lib/rrule.js"></script>
|
||||
|
||||
<script src="/bower_components/angular/angular.min.js"></script>
|
||||
<script src="/bower_components/angular-mocks/angular-mocks.js"></script>
|
||||
<script src="/bower_components/angular-route/angular-route.min.js"></script>
|
||||
<script src="/bower_components/angular-tz-extensions/lib/angular-tz-extensions.js"></script>
|
||||
<script src="/lib/angular-scheduler.js"></script>
|
||||
<script src="/app/js/sampleApp.js"></script>
|
||||
|
||||
<!-- include spec files here... -->
|
||||
<script type="text/javascript" src="GetRRule.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
82
awx/ui/static/lib/angular-scheduler/tests/karma.conf.js
Normal file
82
awx/ui/static/lib/angular-scheduler/tests/karma.conf.js
Normal file
@ -0,0 +1,82 @@
|
||||
// Karma configuration
|
||||
// Generated on Thu Feb 27 2014 22:39:50 GMT-0500 (EST)
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
|
||||
// base path, that will be used to resolve files and exclude
|
||||
basePath: '../',
|
||||
|
||||
|
||||
// frameworks to use
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'bower_components/jquery/dist/jquery.min.js',
|
||||
'bower_components/jqueryui/ui/minified/jquery-ui.min.js',
|
||||
'bower_components/twitter/dist/js/bootstrap.min.js',
|
||||
'bower_components/timezone-js/src/date.js',
|
||||
'bower_components/angular-tz-extensions/packages/jstimezonedetect/jstz.min.js',
|
||||
'bower_components/underscore/underscore.js',
|
||||
'bower_components/rrule/lib/rrule.js',
|
||||
'bower_components/angular/angular.min.js',
|
||||
'bower_components/angular-mocks/angular-mocks.js',
|
||||
'bower_components/angular-route/angular-route.min.js',
|
||||
'bower_components/angular-tz-extensions/lib/angular-tz-extensions.js',
|
||||
'lib/angular-scheduler.js',
|
||||
'tests/GetRRule.js',
|
||||
|
||||
// fixtures
|
||||
{ pattern: 'bower_components/angular-tz-extensions/tz/data/*', watched: false, served: true, included: false }
|
||||
],
|
||||
|
||||
|
||||
// list of files to exclude
|
||||
exclude: [
|
||||
|
||||
],
|
||||
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
|
||||
reporters: ['progress'],
|
||||
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_DEBUG,
|
||||
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
|
||||
|
||||
// Start these browsers, currently available:
|
||||
// - Chrome
|
||||
// - ChromeCanary
|
||||
// - Firefox
|
||||
// - Opera (has to be installed with `npm install karma-opera-launcher`)
|
||||
// - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
|
||||
// - PhantomJS
|
||||
// - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
|
||||
browsers: ['Firefox','Chrome'],
|
||||
|
||||
|
||||
// If browser does not capture in given timeout [ms], kill it
|
||||
captureTimeout: 60000,
|
||||
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, it capture browsers, run tests and exit
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,181 @@
|
||||
/**
|
||||
Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
|
||||
|
||||
If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
|
||||
|
||||
The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
|
||||
|
||||
[jasmine-gem]: http://github.com/pivotal/jasmine-gem
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
/**
|
||||
* ## Require & Instantiate
|
||||
*
|
||||
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
|
||||
*/
|
||||
window.jasmine = jasmineRequire.core(jasmineRequire);
|
||||
|
||||
/**
|
||||
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
|
||||
*/
|
||||
jasmineRequire.html(jasmine);
|
||||
|
||||
/**
|
||||
* Create the Jasmine environment. This is used to run all specs in a project.
|
||||
*/
|
||||
var env = jasmine.getEnv();
|
||||
|
||||
/**
|
||||
* ## The Global Interface
|
||||
*
|
||||
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
|
||||
*/
|
||||
var jasmineInterface = {
|
||||
describe: function(description, specDefinitions) {
|
||||
return env.describe(description, specDefinitions);
|
||||
},
|
||||
|
||||
xdescribe: function(description, specDefinitions) {
|
||||
return env.xdescribe(description, specDefinitions);
|
||||
},
|
||||
|
||||
it: function(desc, func) {
|
||||
return env.it(desc, func);
|
||||
},
|
||||
|
||||
xit: function(desc, func) {
|
||||
return env.xit(desc, func);
|
||||
},
|
||||
|
||||
beforeEach: function(beforeEachFunction) {
|
||||
return env.beforeEach(beforeEachFunction);
|
||||
},
|
||||
|
||||
afterEach: function(afterEachFunction) {
|
||||
return env.afterEach(afterEachFunction);
|
||||
},
|
||||
|
||||
expect: function(actual) {
|
||||
return env.expect(actual);
|
||||
},
|
||||
|
||||
pending: function() {
|
||||
return env.pending();
|
||||
},
|
||||
|
||||
spyOn: function(obj, methodName) {
|
||||
return env.spyOn(obj, methodName);
|
||||
},
|
||||
|
||||
jsApiReporter: new jasmine.JsApiReporter({
|
||||
timer: new jasmine.Timer()
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
|
||||
*/
|
||||
if (typeof window == "undefined" && typeof exports == "object") {
|
||||
extend(exports, jasmineInterface);
|
||||
} else {
|
||||
extend(window, jasmineInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the interface for adding custom equality testers.
|
||||
*/
|
||||
jasmine.addCustomEqualityTester = function(tester) {
|
||||
env.addCustomEqualityTester(tester);
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose the interface for adding custom expectation matchers
|
||||
*/
|
||||
jasmine.addMatchers = function(matchers) {
|
||||
return env.addMatchers(matchers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose the mock interface for the JavaScript timeout functions
|
||||
*/
|
||||
jasmine.clock = function() {
|
||||
return env.clock;
|
||||
};
|
||||
|
||||
/**
|
||||
* ## Runner Parameters
|
||||
*
|
||||
* More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
|
||||
*/
|
||||
|
||||
var queryString = new jasmine.QueryString({
|
||||
getWindowLocation: function() { return window.location; }
|
||||
});
|
||||
|
||||
var catchingExceptions = queryString.getParam("catch");
|
||||
env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
|
||||
|
||||
/**
|
||||
* ## Reporters
|
||||
* The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
|
||||
*/
|
||||
var htmlReporter = new jasmine.HtmlReporter({
|
||||
env: env,
|
||||
onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); },
|
||||
getContainer: function() { return document.body; },
|
||||
createElement: function() { return document.createElement.apply(document, arguments); },
|
||||
createTextNode: function() { return document.createTextNode.apply(document, arguments); },
|
||||
timer: new jasmine.Timer()
|
||||
});
|
||||
|
||||
/**
|
||||
* The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
|
||||
*/
|
||||
env.addReporter(jasmineInterface.jsApiReporter);
|
||||
env.addReporter(htmlReporter);
|
||||
|
||||
/**
|
||||
* Filter which specs will be run by matching the start of the full name against the `spec` query param.
|
||||
*/
|
||||
var specFilter = new jasmine.HtmlSpecFilter({
|
||||
filterString: function() { return queryString.getParam("spec"); }
|
||||
});
|
||||
|
||||
env.specFilter = function(spec) {
|
||||
return specFilter.matches(spec.getFullName());
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack.
|
||||
*/
|
||||
window.setTimeout = window.setTimeout;
|
||||
window.setInterval = window.setInterval;
|
||||
window.clearTimeout = window.clearTimeout;
|
||||
window.clearInterval = window.clearInterval;
|
||||
|
||||
/**
|
||||
* ## Execution
|
||||
*
|
||||
* Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
|
||||
*/
|
||||
var currentWindowOnload = window.onload;
|
||||
|
||||
window.onload = function() {
|
||||
if (currentWindowOnload) {
|
||||
currentWindowOnload();
|
||||
}
|
||||
htmlReporter.initialize();
|
||||
env.execute();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for readability above.
|
||||
*/
|
||||
function extend(destination, source) {
|
||||
for (var property in source) destination[property] = source[property];
|
||||
return destination;
|
||||
}
|
||||
|
||||
}());
|
||||
@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright (c) 2008-2013 Pivotal Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
function getJasmineRequireObj() {
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
return exports;
|
||||
} else {
|
||||
window.jasmineRequire = window.jasmineRequire || {};
|
||||
return window.jasmineRequire;
|
||||
}
|
||||
}
|
||||
|
||||
getJasmineRequireObj().console = function(jRequire, j$) {
|
||||
j$.ConsoleReporter = jRequire.ConsoleReporter();
|
||||
};
|
||||
|
||||
getJasmineRequireObj().ConsoleReporter = function() {
|
||||
|
||||
var noopTimer = {
|
||||
start: function(){},
|
||||
elapsed: function(){ return 0; }
|
||||
};
|
||||
|
||||
function ConsoleReporter(options) {
|
||||
var print = options.print,
|
||||
showColors = options.showColors || false,
|
||||
onComplete = options.onComplete || function() {},
|
||||
timer = options.timer || noopTimer,
|
||||
specCount,
|
||||
failureCount,
|
||||
failedSpecs = [],
|
||||
pendingCount,
|
||||
ansi = {
|
||||
green: '\x1B[32m',
|
||||
red: '\x1B[31m',
|
||||
yellow: '\x1B[33m',
|
||||
none: '\x1B[0m'
|
||||
};
|
||||
|
||||
this.jasmineStarted = function() {
|
||||
specCount = 0;
|
||||
failureCount = 0;
|
||||
pendingCount = 0;
|
||||
print("Started");
|
||||
printNewline();
|
||||
timer.start();
|
||||
};
|
||||
|
||||
this.jasmineDone = function() {
|
||||
printNewline();
|
||||
for (var i = 0; i < failedSpecs.length; i++) {
|
||||
specFailureDetails(failedSpecs[i]);
|
||||
}
|
||||
|
||||
printNewline();
|
||||
var specCounts = specCount + " " + plural("spec", specCount) + ", " +
|
||||
failureCount + " " + plural("failure", failureCount);
|
||||
|
||||
if (pendingCount) {
|
||||
specCounts += ", " + pendingCount + " pending " + plural("spec", pendingCount);
|
||||
}
|
||||
|
||||
print(specCounts);
|
||||
|
||||
printNewline();
|
||||
var seconds = timer.elapsed() / 1000;
|
||||
print("Finished in " + seconds + " " + plural("second", seconds));
|
||||
|
||||
printNewline();
|
||||
|
||||
onComplete(failureCount === 0);
|
||||
};
|
||||
|
||||
this.specDone = function(result) {
|
||||
specCount++;
|
||||
|
||||
if (result.status == "pending") {
|
||||
pendingCount++;
|
||||
print(colored("yellow", "*"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status == "passed") {
|
||||
print(colored("green", '.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status == "failed") {
|
||||
failureCount++;
|
||||
failedSpecs.push(result);
|
||||
print(colored("red", 'F'));
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function printNewline() {
|
||||
print("\n");
|
||||
}
|
||||
|
||||
function colored(color, str) {
|
||||
return showColors ? (ansi[color] + str + ansi.none) : str;
|
||||
}
|
||||
|
||||
function plural(str, count) {
|
||||
return count == 1 ? str : str + "s";
|
||||
}
|
||||
|
||||
function repeat(thing, times) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < times; i++) {
|
||||
arr.push(thing);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function indent(str, spaces) {
|
||||
var lines = (str || '').split("\n");
|
||||
var newArr = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
newArr.push(repeat(" ", spaces).join("") + lines[i]);
|
||||
}
|
||||
return newArr.join("\n");
|
||||
}
|
||||
|
||||
function specFailureDetails(result) {
|
||||
printNewline();
|
||||
print(result.fullName);
|
||||
|
||||
for (var i = 0; i < result.failedExpectations.length; i++) {
|
||||
var failedExpectation = result.failedExpectations[i];
|
||||
printNewline();
|
||||
print(indent(failedExpectation.stack, 2));
|
||||
}
|
||||
|
||||
printNewline();
|
||||
}
|
||||
}
|
||||
|
||||
return ConsoleReporter;
|
||||
};
|
||||
@ -0,0 +1,359 @@
|
||||
/*
|
||||
Copyright (c) 2008-2013 Pivotal Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
jasmineRequire.html = function(j$) {
|
||||
j$.ResultsNode = jasmineRequire.ResultsNode();
|
||||
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
|
||||
j$.QueryString = jasmineRequire.QueryString();
|
||||
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
|
||||
};
|
||||
|
||||
jasmineRequire.HtmlReporter = function(j$) {
|
||||
|
||||
var noopTimer = {
|
||||
start: function() {},
|
||||
elapsed: function() { return 0; }
|
||||
};
|
||||
|
||||
function HtmlReporter(options) {
|
||||
var env = options.env || {},
|
||||
getContainer = options.getContainer,
|
||||
createElement = options.createElement,
|
||||
createTextNode = options.createTextNode,
|
||||
onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {},
|
||||
timer = options.timer || noopTimer,
|
||||
results = [],
|
||||
specsExecuted = 0,
|
||||
failureCount = 0,
|
||||
pendingSpecCount = 0,
|
||||
htmlReporterMain,
|
||||
symbols;
|
||||
|
||||
this.initialize = function() {
|
||||
htmlReporterMain = createDom("div", {className: "html-reporter"},
|
||||
createDom("div", {className: "banner"},
|
||||
createDom("span", {className: "title"}, "Jasmine"),
|
||||
createDom("span", {className: "version"}, j$.version)
|
||||
),
|
||||
createDom("ul", {className: "symbol-summary"}),
|
||||
createDom("div", {className: "alert"}),
|
||||
createDom("div", {className: "results"},
|
||||
createDom("div", {className: "failures"})
|
||||
)
|
||||
);
|
||||
getContainer().appendChild(htmlReporterMain);
|
||||
|
||||
symbols = find(".symbol-summary");
|
||||
};
|
||||
|
||||
var totalSpecsDefined;
|
||||
this.jasmineStarted = function(options) {
|
||||
totalSpecsDefined = options.totalSpecsDefined || 0;
|
||||
timer.start();
|
||||
};
|
||||
|
||||
var summary = createDom("div", {className: "summary"});
|
||||
|
||||
var topResults = new j$.ResultsNode({}, "", null),
|
||||
currentParent = topResults;
|
||||
|
||||
this.suiteStarted = function(result) {
|
||||
currentParent.addChild(result, "suite");
|
||||
currentParent = currentParent.last();
|
||||
};
|
||||
|
||||
this.suiteDone = function(result) {
|
||||
if (currentParent == topResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentParent = currentParent.parent;
|
||||
};
|
||||
|
||||
this.specStarted = function(result) {
|
||||
currentParent.addChild(result, "spec");
|
||||
};
|
||||
|
||||
var failures = [];
|
||||
this.specDone = function(result) {
|
||||
if (result.status != "disabled") {
|
||||
specsExecuted++;
|
||||
}
|
||||
|
||||
symbols.appendChild(createDom("li", {
|
||||
className: result.status,
|
||||
id: "spec_" + result.id,
|
||||
title: result.fullName
|
||||
}
|
||||
));
|
||||
|
||||
if (result.status == "failed") {
|
||||
failureCount++;
|
||||
|
||||
var failure =
|
||||
createDom("div", {className: "spec-detail failed"},
|
||||
createDom("div", {className: "description"},
|
||||
createDom("a", {title: result.fullName, href: specHref(result)}, result.fullName)
|
||||
),
|
||||
createDom("div", {className: "messages"})
|
||||
);
|
||||
var messages = failure.childNodes[1];
|
||||
|
||||
for (var i = 0; i < result.failedExpectations.length; i++) {
|
||||
var expectation = result.failedExpectations[i];
|
||||
messages.appendChild(createDom("div", {className: "result-message"}, expectation.message));
|
||||
messages.appendChild(createDom("div", {className: "stack-trace"}, expectation.stack));
|
||||
}
|
||||
|
||||
failures.push(failure);
|
||||
}
|
||||
|
||||
if (result.status == "pending") {
|
||||
pendingSpecCount++;
|
||||
}
|
||||
};
|
||||
|
||||
this.jasmineDone = function() {
|
||||
var banner = find(".banner");
|
||||
banner.appendChild(createDom("span", {className: "duration"}, "finished in " + timer.elapsed() / 1000 + "s"));
|
||||
|
||||
var alert = find(".alert");
|
||||
|
||||
alert.appendChild(createDom("span", { className: "exceptions" },
|
||||
createDom("label", { className: "label", 'for': "raise-exceptions" }, "raise exceptions"),
|
||||
createDom("input", {
|
||||
className: "raise",
|
||||
id: "raise-exceptions",
|
||||
type: "checkbox"
|
||||
})
|
||||
));
|
||||
var checkbox = find("input");
|
||||
|
||||
checkbox.checked = !env.catchingExceptions();
|
||||
checkbox.onclick = onRaiseExceptionsClick;
|
||||
|
||||
if (specsExecuted < totalSpecsDefined) {
|
||||
var skippedMessage = "Ran " + specsExecuted + " of " + totalSpecsDefined + " specs - run all";
|
||||
alert.appendChild(
|
||||
createDom("span", {className: "bar skipped"},
|
||||
createDom("a", {href: "?", title: "Run all specs"}, skippedMessage)
|
||||
)
|
||||
);
|
||||
}
|
||||
var statusBarMessage = "" + pluralize("spec", specsExecuted) + ", " + pluralize("failure", failureCount);
|
||||
if (pendingSpecCount) { statusBarMessage += ", " + pluralize("pending spec", pendingSpecCount); }
|
||||
|
||||
var statusBarClassName = "bar " + ((failureCount > 0) ? "failed" : "passed");
|
||||
alert.appendChild(createDom("span", {className: statusBarClassName}, statusBarMessage));
|
||||
|
||||
var results = find(".results");
|
||||
results.appendChild(summary);
|
||||
|
||||
summaryList(topResults, summary);
|
||||
|
||||
function summaryList(resultsTree, domParent) {
|
||||
var specListNode;
|
||||
for (var i = 0; i < resultsTree.children.length; i++) {
|
||||
var resultNode = resultsTree.children[i];
|
||||
if (resultNode.type == "suite") {
|
||||
var suiteListNode = createDom("ul", {className: "suite", id: "suite-" + resultNode.result.id},
|
||||
createDom("li", {className: "suite-detail"},
|
||||
createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description)
|
||||
)
|
||||
);
|
||||
|
||||
summaryList(resultNode, suiteListNode);
|
||||
domParent.appendChild(suiteListNode);
|
||||
}
|
||||
if (resultNode.type == "spec") {
|
||||
if (domParent.getAttribute("class") != "specs") {
|
||||
specListNode = createDom("ul", {className: "specs"});
|
||||
domParent.appendChild(specListNode);
|
||||
}
|
||||
specListNode.appendChild(
|
||||
createDom("li", {
|
||||
className: resultNode.result.status,
|
||||
id: "spec-" + resultNode.result.id
|
||||
},
|
||||
createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
alert.appendChild(
|
||||
createDom('span', {className: "menu bar spec-list"},
|
||||
createDom("span", {}, "Spec List | "),
|
||||
createDom('a', {className: "failures-menu", href: "#"}, "Failures")));
|
||||
alert.appendChild(
|
||||
createDom('span', {className: "menu bar failure-list"},
|
||||
createDom('a', {className: "spec-list-menu", href: "#"}, "Spec List"),
|
||||
createDom("span", {}, " | Failures ")));
|
||||
|
||||
find(".failures-menu").onclick = function() {
|
||||
setMenuModeTo('failure-list');
|
||||
};
|
||||
find(".spec-list-menu").onclick = function() {
|
||||
setMenuModeTo('spec-list');
|
||||
};
|
||||
|
||||
setMenuModeTo('failure-list');
|
||||
|
||||
var failureNode = find(".failures");
|
||||
for (var i = 0; i < failures.length; i++) {
|
||||
failureNode.appendChild(failures[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function find(selector) {
|
||||
return getContainer().querySelector(selector);
|
||||
}
|
||||
|
||||
function createDom(type, attrs, childrenVarArgs) {
|
||||
var el = createElement(type);
|
||||
|
||||
for (var i = 2; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(createTextNode(child));
|
||||
} else {
|
||||
if (child) {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var attr in attrs) {
|
||||
if (attr == "className") {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function pluralize(singular, count) {
|
||||
var word = (count == 1 ? singular : singular + "s");
|
||||
|
||||
return "" + count + " " + word;
|
||||
}
|
||||
|
||||
function specHref(result) {
|
||||
return "?spec=" + encodeURIComponent(result.fullName);
|
||||
}
|
||||
|
||||
function setMenuModeTo(mode) {
|
||||
htmlReporterMain.setAttribute("class", "html-reporter " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
return HtmlReporter;
|
||||
};
|
||||
|
||||
jasmineRequire.HtmlSpecFilter = function() {
|
||||
function HtmlSpecFilter(options) {
|
||||
var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||
var filterPattern = new RegExp(filterString);
|
||||
|
||||
this.matches = function(specName) {
|
||||
return filterPattern.test(specName);
|
||||
};
|
||||
}
|
||||
|
||||
return HtmlSpecFilter;
|
||||
};
|
||||
|
||||
jasmineRequire.ResultsNode = function() {
|
||||
function ResultsNode(result, type, parent) {
|
||||
this.result = result;
|
||||
this.type = type;
|
||||
this.parent = parent;
|
||||
|
||||
this.children = [];
|
||||
|
||||
this.addChild = function(result, type) {
|
||||
this.children.push(new ResultsNode(result, type, this));
|
||||
};
|
||||
|
||||
this.last = function() {
|
||||
return this.children[this.children.length - 1];
|
||||
};
|
||||
}
|
||||
|
||||
return ResultsNode;
|
||||
};
|
||||
|
||||
jasmineRequire.QueryString = function() {
|
||||
function QueryString(options) {
|
||||
|
||||
this.setParam = function(key, value) {
|
||||
var paramMap = queryStringToParamMap();
|
||||
paramMap[key] = value;
|
||||
options.getWindowLocation().search = toQueryString(paramMap);
|
||||
};
|
||||
|
||||
this.getParam = function(key) {
|
||||
return queryStringToParamMap()[key];
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function toQueryString(paramMap) {
|
||||
var qStrPairs = [];
|
||||
for (var prop in paramMap) {
|
||||
qStrPairs.push(encodeURIComponent(prop) + "=" + encodeURIComponent(paramMap[prop]));
|
||||
}
|
||||
return "?" + qStrPairs.join('&');
|
||||
}
|
||||
|
||||
function queryStringToParamMap() {
|
||||
var paramStr = options.getWindowLocation().search.substring(1),
|
||||
params = [],
|
||||
paramMap = {};
|
||||
|
||||
if (paramStr.length > 0) {
|
||||
params = paramStr.split('&');
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
var value = decodeURIComponent(p[1]);
|
||||
if (value === "true" || value === "false") {
|
||||
value = JSON.parse(value);
|
||||
}
|
||||
paramMap[decodeURIComponent(p[0])] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return QueryString;
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
|
||||
|
||||
.html-reporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
|
||||
.html-reporter a { text-decoration: none; }
|
||||
.html-reporter a:hover { text-decoration: underline; }
|
||||
.html-reporter p, .html-reporter h1, .html-reporter h2, .html-reporter h3, .html-reporter h4, .html-reporter h5, .html-reporter h6 { margin: 0; line-height: 14px; }
|
||||
.html-reporter .banner, .html-reporter .symbol-summary, .html-reporter .summary, .html-reporter .result-message, .html-reporter .spec .description, .html-reporter .spec-detail .description, .html-reporter .alert .bar, .html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; }
|
||||
.html-reporter .banner .version { margin-left: 14px; }
|
||||
.html-reporter #jasmine_content { position: fixed; right: 100%; }
|
||||
.html-reporter .version { color: #aaaaaa; }
|
||||
.html-reporter .banner { margin-top: 14px; }
|
||||
.html-reporter .duration { color: #aaaaaa; float: right; }
|
||||
.html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; }
|
||||
.html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; }
|
||||
.html-reporter .symbol-summary li.passed { font-size: 14px; }
|
||||
.html-reporter .symbol-summary li.passed:before { color: #5e7d00; content: "\02022"; }
|
||||
.html-reporter .symbol-summary li.failed { line-height: 9px; }
|
||||
.html-reporter .symbol-summary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
|
||||
.html-reporter .symbol-summary li.disabled { font-size: 14px; }
|
||||
.html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; }
|
||||
.html-reporter .symbol-summary li.pending { line-height: 17px; }
|
||||
.html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; }
|
||||
.html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
|
||||
.html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
|
||||
.html-reporter .bar.failed { background-color: #b03911; }
|
||||
.html-reporter .bar.passed { background-color: #a6b779; }
|
||||
.html-reporter .bar.skipped { background-color: #bababa; }
|
||||
.html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; }
|
||||
.html-reporter .bar.menu a { color: #333333; }
|
||||
.html-reporter .bar a { color: white; }
|
||||
.html-reporter.spec-list .bar.menu.failure-list, .html-reporter.spec-list .results .failures { display: none; }
|
||||
.html-reporter.failure-list .bar.menu.spec-list, .html-reporter.failure-list .summary { display: none; }
|
||||
.html-reporter .running-alert { background-color: #666666; }
|
||||
.html-reporter .results { margin-top: 14px; }
|
||||
.html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
|
||||
.html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
|
||||
.html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
|
||||
.html-reporter.showDetails .summary { display: none; }
|
||||
.html-reporter.showDetails #details { display: block; }
|
||||
.html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
|
||||
.html-reporter .summary { margin-top: 14px; }
|
||||
.html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; }
|
||||
.html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; }
|
||||
.html-reporter .summary li.passed a { color: #5e7d00; }
|
||||
.html-reporter .summary li.failed a { color: #b03911; }
|
||||
.html-reporter .summary li.pending a { color: #ba9d37; }
|
||||
.html-reporter .description + .suite { margin-top: 0; }
|
||||
.html-reporter .suite { margin-top: 14px; }
|
||||
.html-reporter .suite a { color: #333333; }
|
||||
.html-reporter .failures .spec-detail { margin-bottom: 28px; }
|
||||
.html-reporter .failures .spec-detail .description { background-color: #b03911; }
|
||||
.html-reporter .failures .spec-detail .description a { color: white; }
|
||||
.html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; }
|
||||
.html-reporter .result-message span.result { display: block; }
|
||||
.html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
117
awx/ui/static/lib/angular-scheduler/tests/protractorConf.js
vendored
Normal file
117
awx/ui/static/lib/angular-scheduler/tests/protractorConf.js
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
// A reference configuration file.
|
||||
exports.config = {
|
||||
// The address of a running selenium server. If specified, Protractor will
|
||||
// connect to an already running instance of selenium. This usually looks like
|
||||
// seleniumAddress: 'http://localhost:4444/wd/hub'
|
||||
seleniumAddress: 'http://localhost:4444/wd/hub',
|
||||
|
||||
// The timeout for each script run on the browser. This should be longer
|
||||
// than the maximum time your application needs to stabilize between tasks.
|
||||
allScriptsTimeout: 11000,
|
||||
|
||||
// ----- What tests to run -----
|
||||
//
|
||||
// Spec patterns are relative to the location of this config.
|
||||
specs: [
|
||||
'SetRRule.js',
|
||||
],
|
||||
|
||||
// Patterns to exclude.
|
||||
exclude: [],
|
||||
|
||||
// ----- Capabilities to be passed to the webdriver instance ----
|
||||
//
|
||||
// For a full list of available capabilities, see
|
||||
// https://code.google.com/p/selenium/wiki/DesiredCapabilities
|
||||
// and
|
||||
// https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
|
||||
// If you would like to run more than one instance of webdriver on the same
|
||||
// tests, use multiCapabilities, which takes an array of capabilities.
|
||||
// If this is specified, capabilities will be ignored.
|
||||
multiCapabilities: [],
|
||||
|
||||
// ----- More information for your tests ----
|
||||
//
|
||||
// A base URL for your application under test. Calls to protractor.get()
|
||||
// with relative paths will be prepended with this.
|
||||
baseUrl: 'http://localhost:8000',
|
||||
|
||||
// Selector for the element housing the angular app - this defaults to
|
||||
// body, but is necessary if ng-app is on a descendant of <body>
|
||||
rootElement: 'html',
|
||||
|
||||
// A callback function called once protractor is ready and available, and
|
||||
// before the specs are executed
|
||||
// You can specify a file containing code to run by setting onPrepare to
|
||||
// the filename string.
|
||||
onPrepare: function() {
|
||||
// At this point, global 'protractor' object will be set up, and jasmine
|
||||
// will be available. For example, you can add a Jasmine reporter with:
|
||||
// jasmine.getEnv().addReporter(new jasmine.JUnitXmlReporter(
|
||||
// 'outputdir/', true, true));
|
||||
},
|
||||
|
||||
// The params object will be passed directly to the protractor instance,
|
||||
// and can be accessed from your test. It is an arbitrary object and can
|
||||
// contain anything you may need in your test.
|
||||
// This can be changed via the command line as:
|
||||
// --params.login.user 'Joe'
|
||||
params: {
|
||||
login: {
|
||||
user: 'Jane',
|
||||
password: '1234'
|
||||
}
|
||||
},
|
||||
|
||||
// ----- The test framework -----
|
||||
//
|
||||
// Jasmine and Cucumber are fully supported as a test and assertion framework.
|
||||
// Mocha has limited beta support. You will need to include your own
|
||||
// assertion framework if working with mocha.
|
||||
framework: 'jasmine',
|
||||
|
||||
// ----- Options to be passed to minijasminenode -----
|
||||
//
|
||||
// See the full list at https://github.com/juliemr/minijasminenode
|
||||
jasmineNodeOpts: {
|
||||
// onComplete will be called just before the driver quits.
|
||||
onComplete: null,
|
||||
// If true, display spec names.
|
||||
isVerbose: false,
|
||||
// If true, print colors to the terminal.
|
||||
showColors: true,
|
||||
// If true, include stack traces in failures.
|
||||
includeStackTrace: true,
|
||||
// Default time to wait in ms before a test fails.
|
||||
defaultTimeoutInterval: 30000
|
||||
},
|
||||
|
||||
// ----- Options to be passed to mocha -----
|
||||
//
|
||||
// See the full list at http://visionmedia.github.io/mocha/
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
reporter: 'list'
|
||||
},
|
||||
|
||||
// ----- Options to be passed to cucumber -----
|
||||
cucumberOpts: {
|
||||
// Require files before executing the features.
|
||||
require: 'cucumber/stepDefinitions.js',
|
||||
// Only execute the features or scenarios with tags matching @dev.
|
||||
tags: '@dev',
|
||||
// How to format features (default: progress)
|
||||
format: 'summary'
|
||||
},
|
||||
|
||||
// ----- The cleanup step -----
|
||||
//
|
||||
// A callback function called once the tests have finished running and
|
||||
// the webdriver instance has been shut down. It is passed the exit code
|
||||
// (0 if the tests passed or 1 if not).
|
||||
onCleanUp: function() {}
|
||||
};
|
||||
58
awx/ui/static/lib/angular-scheduler/tests/spec/PlayerSpec.js
vendored
Normal file
58
awx/ui/static/lib/angular-scheduler/tests/spec/PlayerSpec.js
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
describe("Player", function() {
|
||||
var player;
|
||||
var song;
|
||||
|
||||
beforeEach(function() {
|
||||
player = new Player();
|
||||
song = new Song();
|
||||
});
|
||||
|
||||
it("should be able to play a Song", function() {
|
||||
player.play(song);
|
||||
expect(player.currentlyPlayingSong).toEqual(song);
|
||||
|
||||
//demonstrates use of custom matcher
|
||||
expect(player).toBePlaying(song);
|
||||
});
|
||||
|
||||
describe("when song has been paused", function() {
|
||||
beforeEach(function() {
|
||||
player.play(song);
|
||||
player.pause();
|
||||
});
|
||||
|
||||
it("should indicate that the song is currently paused", function() {
|
||||
expect(player.isPlaying).toBeFalsy();
|
||||
|
||||
// demonstrates use of 'not' with a custom matcher
|
||||
expect(player).not.toBePlaying(song);
|
||||
});
|
||||
|
||||
it("should be possible to resume", function() {
|
||||
player.resume();
|
||||
expect(player.isPlaying).toBeTruthy();
|
||||
expect(player.currentlyPlayingSong).toEqual(song);
|
||||
});
|
||||
});
|
||||
|
||||
// demonstrates use of spies to intercept and test method calls
|
||||
it("tells the current song if the user has made it a favorite", function() {
|
||||
spyOn(song, 'persistFavoriteStatus');
|
||||
|
||||
player.play(song);
|
||||
player.makeFavorite();
|
||||
|
||||
expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
//demonstrates use of expected exceptions
|
||||
describe("#resume", function() {
|
||||
it("should throw an exception if song is already playing", function() {
|
||||
player.play(song);
|
||||
|
||||
expect(function() {
|
||||
player.resume();
|
||||
}).toThrowError("song is already playing");
|
||||
});
|
||||
});
|
||||
});
|
||||
15
awx/ui/static/lib/angular-scheduler/tests/spec/SpecHelper.js
vendored
Normal file
15
awx/ui/static/lib/angular-scheduler/tests/spec/SpecHelper.js
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
beforeEach(function () {
|
||||
jasmine.addMatchers({
|
||||
toBePlaying: function () {
|
||||
return {
|
||||
compare: function (actual, expected) {
|
||||
var player = actual;
|
||||
|
||||
return {
|
||||
pass: player.currentlyPlayingSong === expected && player.isPlaying
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -32,15 +32,16 @@
|
||||
<h4 ng-bind="schedulesTitle"></h4>
|
||||
<button type="button" class="close pull-right" ng-click="cancelScheduleForm()">×</button>
|
||||
</div>
|
||||
<div id="schedules-form"></div>
|
||||
<div id="schedules-form-container-body">
|
||||
<div id="schedules-form"></div>
|
||||
<div id="schedules-detail"></div>
|
||||
</div>
|
||||
<div id="schedules-buttons">
|
||||
<a id="schedules-flip-link" ng-click="scheduleDetailRotate()" href=""><i class="fa fa-list-ul"> Show Details</i></a>
|
||||
<button type="button" class="btn btn-default btn-sm" id="reset-button" ng-click="cancelScheduleForm()"><i class="fa fa-times"></i> Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="save-button" ng-click="saveScheduleForm()"><i class="fa fa-check"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="schedules-detail">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,35 +18,6 @@
|
||||
<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>
|
||||
@ -15,7 +15,7 @@
|
||||
"angular-md5": "~0.1.7",
|
||||
"angular-resource": "~1.2.12",
|
||||
"angular-sanitize": "~1.2.12",
|
||||
"angular-scheduler": "*",
|
||||
"angular-scheduler": "~0.0.3",
|
||||
"bootstrap": "~3.1.1",
|
||||
"components-font-awesome": "~4.0.3",
|
||||
"less.js": "~1.6.3",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user