1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-30 22:21:13 +03:00

Merge pull request #42 from joefiorini/job-status-graph

Fixes responsiveness of graphs on dashboard
This commit is contained in:
Joe Fiorini 2015-01-30 15:50:59 -05:00
commit 6a4104e913
43 changed files with 1193 additions and 869 deletions

3
.gitignore vendored
View File

@ -6,6 +6,7 @@ awx/projects
awx/job_output
awx/public/media
awx/public/static
awx/ui/tests/test-results.xml
awx/ui/static/js/awx.min.js
awx/ui/static/js/local_config.js
awx/ui/static/css/awx.min.css
@ -26,7 +27,7 @@ tar-build
*.py[c,o]
# JavaScript
/GruntFile.js
/Gruntfile.js
/bower.json
/package.json
node_modules/**

View File

@ -1,21 +1,31 @@
{
// 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":false, "$AnsibleConfig":true, "$basePath":true, "jsyaml":false, "_":false, "d3":false, "Donut3D":false, "nv":false },
"curly": true,
"immed": true,
"latedef": "nofunc",
"noarg": true,
"nonew": true,
"notypeof": true,
"globals": {
"angular":false,
"alert":false,
"$AnsibleConfig":true,
"$basePath":true,
"jsyaml":false,
"_":false,
"d3":false,
"Donut3D":false,
"nv":false
},
"strict": false,
"quotmark": false,
"smarttabs": true,
"trailing": true,
"undef": true,
"unused": true,
"eqeqeq": true,
"indent": 4,
"onevar": true,
"newcap": false
}

View File

@ -66,7 +66,7 @@ MOCK_CFG ?=
.PHONY: clean rebase push requirements requirements_pypi requirements_jenkins \
develop refresh adduser syncdb migrate dbchange dbshell runserver celeryd \
receiver test test_coverage coverage_html test_ui test_jenkins dev_build \
receiver test test_coverage coverage_html ui_analysis_report test_ui test_jenkins dev_build \
release_build release_clean sdist rpmtar mock-rpm mock-srpm \
deb deb-src debian reprepro setup_tarball
@ -240,9 +240,12 @@ test_coverage:
coverage_html:
coverage html
# Run UI unit tests using Selenium.
test_ui:
$(PYTHON) manage.py test -v2 awx.ui.tests
ui_analysis_report: node_modules
$(GRUNT) plato:report
# Run UI unit tests
test_ui: node_modules
$(GRUNT) karma:ci
# Run API unit tests across multiple Python/Django versions with Tox.
test_tox:

View File

@ -18,11 +18,14 @@ if ($basePath) {
urlPrefix = $basePath;
}
angular.module('Tower', [
'ngRoute',
'ngSanitize',
'ngCookies',
'RestServices',
'DataServices',
'DashboardGraphs',
'AuthService',
'Utilities',
'LicenseHelper',
@ -84,9 +87,6 @@ angular.module('Tower', [
'SelectionHelper',
'HostGroupsFormDefinition',
'DashboardCountsWidget',
'JobStatusGraphWidget',
'HostPieChartWidget',
'HostGraphWidget',
'DashboardJobsWidget',
'PortalJobsWidget',
'StreamWidget',
@ -131,7 +131,6 @@ angular.module('Tower', [
.constant('AngularScheduler.useTimezone', true)
.constant('AngularScheduler.showUTCField', true)
.constant('$timezones.definitions.location', urlPrefix + 'lib/angular-tz-extensions/tz/data')
.config(['$routeProvider',
function ($routeProvider) {
@ -399,7 +398,15 @@ angular.module('Tower', [
when('/home', {
templateUrl: urlPrefix + 'partials/home.html',
controller: 'Home'
controller: 'Home',
resolve: {
graphData: function($q, jobStatusGraphData, hostCountGraphData) {
return $q.all({
jobStatus: jobStatusGraphData.get("month", "all"),
hostCounts: hostCountGraphData.get()
});
}
}
}).
when('/home/groups', {

View File

@ -28,7 +28,7 @@
// > password_strength = green
// It also controls password validation. Passwords are rejected if the score is not > password_strength.
session_timeout: 1800, // Number of seconds before an inactive session is automatically timed out and forced to log in again.
session_timeout: 1800, // Number of seconds before an inactive session is automatically timed out and forced to log in again.
// Separate from time out value set in API.

View File

@ -25,12 +25,12 @@
* Host count graph should only be loaded if the user is a super user
*
*/
function Home($scope, $compile, $routeParams, $rootScope, $location, $log, Wait, DashboardCounts, HostGraph, JobStatusGraph, HostPieChart, DashboardJobs,
ClearScope, Stream, Rest, GetBasePath, ProcessErrors, Button){
function Home($scope, $compile, $routeParams, $rootScope, $location, $log, Wait, DashboardCounts, DashboardJobs,
ClearScope, Stream, Rest, GetBasePath, ProcessErrors, Button, $window, graphData){
ClearScope('home');
var buttons, html, e, waitCount, loadedCount,borderStyles, jobs_scope, schedule_scope;
var buttons, html, e, borderStyles;
// Add buttons to the top of the Home page. We're using lib/ansible/generator_helpers.js-> Buttons()
// to build buttons dynamically and insure all styling and icons match the rest of the application.
@ -64,39 +64,16 @@ function Home($scope, $compile, $routeParams, $rootScope, $location, $log, Wait,
e.html(html);
$compile(e)($scope);
waitCount = 4;
loadedCount = 0;
if (!$routeParams.login) {
// If we're not logging in, start the Wait widget. Otherwise, it's already running.
//Wait('start');
}
if ($scope.removeWidgetLoaded) {
$scope.removeWidgetLoaded();
}
$scope.removeWidgetLoaded = $scope.$on('WidgetLoaded', function (e, label, jobscope, schedulescope) {
// Once all the widgets report back 'loaded', turn off Wait widget
if(label==="dashboard_jobs"){
jobs_scope = jobscope;
schedule_scope = schedulescope;
}
loadedCount++;
if (loadedCount === waitCount) {
$(window).resize(_.debounce(function() {
$scope.$emit('ResizeJobGraph');
$scope.$emit('ResizeHostGraph');
$scope.$emit('ResizeHostPieGraph');
Wait('stop');
}, 500));
$(window).resize();
}
});
if ($scope.removeDashboardReady) {
$scope.removeDashboardReady();
}
$scope.removeDashboardReady = $scope.$on('dashboardReady', function (e, data) {
nv.dev=false;
@ -106,64 +83,24 @@ function Home($scope, $compile, $routeParams, $rootScope, $location, $log, Wait,
"margin-bottom": "15px"};
$('.graph-container').css(borderStyles);
var winHeight = $(window).height(),
available_height = winHeight - $('#main-menu-container .navbar').outerHeight() - $('#count-container').outerHeight() - 120;
$('.graph-container').height(available_height/2);
// // chart.update();
DashboardCounts({
scope: $scope,
target: 'dash-counts',
dashboard: data
});
JobStatusGraph({
scope: $scope,
target: 'dash-job-status-graph',
dashboard: data
});
// // chart.update();
$scope.graphData = graphData;
if ($rootScope.user_is_superuser === true) {
waitCount = 5;
HostGraph({
scope: $scope,
target: 'dash-host-count-graph',
dashboard: data
});
}
else{
$('#dash-host-count-graph').remove(); //replaceWith("<div id='dash-host-count-graph' class='left-side col-sm-12 col-xs-12'></div>");
}
DashboardJobs({
scope: $scope,
target: 'dash-jobs-list',
dashboard: data
});
HostPieChart({
scope: $scope,
target: 'dash-host-status-graph',
dashboard: data
});
});
if ($rootScope.removeJobStatusChange) {
$rootScope.removeJobStatusChange();
}
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function() {
jobs_scope.refreshJobs();
$scope.$emit('ReloadJobStatusGraph');
});
if ($rootScope.removeScheduleChange) {
$rootScope.removeScheduleChange();
}
$rootScope.removeScheduleChange = $rootScope.$on('ScheduleChange', function() {
schedule_scope.refreshSchedules();
$scope.$emit('ReloadJobStatusGraph');
});
$scope.showActivity = function () {
Stream({
scope: $scope
@ -172,23 +109,23 @@ function Home($scope, $compile, $routeParams, $rootScope, $location, $log, Wait,
$scope.refresh = function () {
Wait('start');
loadedCount = 0;
Rest.setUrl(GetBasePath('dashboard'));
Rest.get()
.success(function (data) {
$scope.$emit('dashboardReady', data);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard: ' + status });
});
.success(function (data) {
$scope.dashboardData = data;
$scope.$emit('dashboardReady', data);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard: ' + status });
});
};
$scope.refresh();
}
Home.$inject = ['$scope', '$compile', '$routeParams', '$rootScope', '$location', '$log','Wait', 'DashboardCounts', 'HostGraph','JobStatusGraph', 'HostPieChart', 'DashboardJobs',
'ClearScope', 'Stream', 'Rest', 'GetBasePath', 'ProcessErrors', 'Button'
Home.$inject = ['$scope', '$compile', '$routeParams', '$rootScope', '$location', '$log','Wait', 'DashboardCounts', 'DashboardJobs',
'ClearScope', 'Stream', 'Rest', 'GetBasePath', 'ProcessErrors', 'Button', '$window', 'graphData'
];
@ -757,4 +694,4 @@ function HomeHosts($scope, $location, $routeParams, HomeHostList, GenerateList,
HomeHosts.$inject = ['$scope', '$location', '$routeParams', 'HomeHostList', 'GenerateList', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller',
'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'SetStatus', 'ToggleHostEnabled', 'HostsEdit', 'Stream',
'Find', 'ShowJobSummary', 'ViewJob'
];
];

View File

@ -527,7 +527,9 @@ function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $routePa
if($scope.survey_enabled === true && $scope.survey_exists!==true){
$scope.$emit("PromptForSurvey");
} else $scope.$emit("GatherFormFields");
} else {
$scope.$emit("GatherFormFields");
}
};
@ -634,7 +636,9 @@ function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, $routeP
' project or make the playbooks available on the file system.', 'alert-info');
});
}
else Wait('stop');
else {
Wait('stop');
}
};
// Detect and alert user to potential SCM status issues
@ -964,7 +968,9 @@ function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, $routeP
if($scope.survey_enabled === true && $scope.survey_exists!==true){
$scope.$emit("PromptForSurvey");
} else $scope.$emit("GatherFormFields");
} else {
$scope.$emit("GatherFormFields");
}
};
@ -1021,11 +1027,12 @@ function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, $routeP
if($scope.survey_enabled === true && $scope.survey_exists!==true){
$scope.$emit("PromptForSurvey");
}
else
else {
PlaybookRun({
scope: $scope,
id: id
});
}
};
// handler for 'Enable Survey' button
@ -1063,4 +1070,4 @@ JobTemplatesEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$l
'GetBasePath', 'md5Setup', 'ParseTypeChange', 'JobStatusToolTip', 'FormatDate', 'Wait', 'Stream', 'Empty', 'Prompt',
'ParseVariableString', 'ToJSON', 'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate', 'GetChoices',
'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit', 'PlaybookRun' , 'SurveyControllerInit', '$sce'
];
];

View File

@ -0,0 +1,53 @@
angular.module('DashboardGraphs')
.directive('autoSizeModule', ['$window', '$timeout', function($window, $timeout) {
// Adjusts the size of the module so that all modules
// fit into a single a page; assumes there are 2 rows
// of modules, with the available height being offset
// by the navbar & the count summaries module
return function(scope, element) {
// We need to trigger a resize on the first call
// to this when the view things load; but we don't want
// to trigger a global window resize for everything that
// has an auto resize, since they'll all pick it up with
// a single call
var triggerResize =
_.throttle(function() {
$($window).resize();
}, 1000);
function adjustSizeInitially() {
adjustSize();
triggerResize();
}
function adjustSize() {
var winHeight = $($window).height(),
available_height = winHeight - $('#main-menu-container .navbar').outerHeight() - $('#count-container').outerHeight() - 120;
element.height(available_height/2);
}
$($window).resize(adjustSize);
element.on('$destroy', function() {
$($window).off('resize', adjustSize);
});
// Wait a second or until dashboardReady triggers,
// whichever comes first. The timeout handles cases
// where dashboardReady never fires.
var dashboardReadyTimeout = $timeout(adjustSizeInitially, 500);
// This makes sure count-container div is loaded
// by controllers/Home.js before we use it
// to determine the available window height
scope.$on('dashboardReady', function() {
$timeout.cancel(dashboardReadyTimeout);
adjustSizeInitially();
});
};
}]);

View File

@ -0,0 +1 @@
angular.module('DashboardGraphs', []);

View File

@ -0,0 +1,126 @@
angular.module('DashboardGraphs').
directive('hostCountGraph', ['GetBasePath', 'Rest', 'adjustGraphSize', '$window', function(getBasePath, Rest, adjustGraphSize, $window) {
return {
restrict: 'E',
templateUrl: '/static/partials/host_count_graph.html',
link: link
};
function link(scope, element, attr) {
var license_graph;
scope.$watch(attr.data, function(data) {
if(!data) {
return;
}
createGraph(data.hosts, data.license);
});
function onResize() {
if(!license_graph) {
return;
}
adjustGraphSize(license_graph, element);
}
angular.element($window).on('resize', function() {
if(!license_graph) {
return;
}
adjustGraphSize(license_graph, element);
});
element.on('$destroy', function() {
angular.element($window).off('resize', onResize);
});
function createGraph(data, license) {
//url = getBasePath('dashboard')+'graphs/';
var graphData = [
{ "key" : "Hosts" ,
"color" : "#1778c3",
"values": data.hosts
},
{ "key" : "License" ,
"color" : "#171717",
"values": data.hosts
}
];
graphData.map(function(series) {
if(series.key==="Hosts"){
series.values = series.values.map(function(d) {
return {
x: d[0],
y: d[1]
};
});
}
if(series.key==="License"){
series.values = series.values.map(function(d) {
return {
x: d[0],
y: license
};
});
}
return series;
});
var width = $('.graph-container').width(), // nv.utils.windowSize().width/3,
height = $('.graph-container').height()*0.6; //nv.utils.windowSize().height/5,
license_graph = nv.models.lineChart()
.margin({top: 15, right: 75, bottom: 40, left: 85})
.x(function(d,i) { return i ;})
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
.transitionDuration(350) //how fast do you want the lines to transition?
.showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showYAxis(true) //Show the y-axis
.showXAxis(true) //Show the x-axis
;
license_graph.xAxis
.axisLabel("Time")
.tickFormat(function(d) {
var dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
return dx ? d3.time.format('%m/%d')(new Date(Number(dx+'000'))) : '';
});
license_graph.yAxis //Chart y-axis settings
.axisLabel('Hosts')
.tickFormat(d3.format('.f'));
d3.select(element.find('svg')[0])
.datum(graphData).transition()
.attr('width', width)
.attr('height', height)
.duration(500)
.call(license_graph)
.style({
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/fonts/OpenSans-Regular.ttf)"
});
scope.$emit('WidgetLoaded');
adjustGraphSize(license_graph, element);
return license_graph;
}
}
}]);

View File

@ -0,0 +1,102 @@
angular.module('DashboardGraphs')
.directive('hostStatusGraph', ['$compile', '$window',
function ($compile, $window) {
return {
restrict: 'E',
link: link,
templateUrl: '/static/partials/host_status_graph.html'
};
function link(scope, element, attr) {
var host_pie_chart;
scope.$watch(attr.data, function(data) {
if (data && data.hosts) {
createGraph(data);
}
});
function adjustGraphSize() {
if (angular.isUndefined(host_pie_chart)) {
return;
}
var parentHeight = element.parent().parent().height();
var toolbarHeight = element.find('.toolbar').height();
var container = element.find('svg').parent();
var margins = host_pie_chart.margin();
var newHeight = parentHeight - toolbarHeight - margins.bottom;
$(container).height(newHeight);
host_pie_chart.update();
}
angular.element($window).on('resize', adjustGraphSize);
element.on('$destroy', function() {
angular.element($window).off('resize', adjustGraphSize);
});
function createGraph(data) {
if(data.hosts.total+data.hosts.failed>0){
data = [
{ "label": "Successful",
"color": "#00aa00",
"value" : data.hosts.total
} ,
{ "label": "Failed",
"color" : "#aa0000",
"value" : data.hosts.failed
}
];
host_pie_chart = nv.models.pieChart()
.margin({top: 5, right: 75, bottom: 25, left: 85})
.x(function(d) { return d.label; })
.y(function(d) { return d.value; })
.showLabels(true)
.labelThreshold(0.01)
.tooltipContent(function(x, y) {
return '<b>'+x+'</b>'+ '<p>' + Math.floor(y.replace(',','')) + ' Hosts ' + '</p>';
})
.color(['#00aa00', '#aa0000']);
host_pie_chart.pie.pieLabelsOutside(true).labelType("percent");
d3.select(element.find('svg')[0])
.datum(data)
.transition().duration(350)
.call(host_pie_chart)
.style({
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/fonts/OpenSans-Regular.ttf)"
});
adjustGraphSize();
return host_pie_chart;
}
else{
// This should go in a template or something
// but I'm at the end of a card and need to get this done.
// We definitely need to refactor this, I'm letting
// good enough be good enough for right now.
var notFoundContainer = $('<div></div>');
notFoundContainer.css({
'text-align': 'center',
'width': '100%',
'padding-top': '2em'
});
notFoundContainer.text('No host data');
element.find('svg').replaceWith(notFoundContainer);
}
}
}
}]);

View File

@ -0,0 +1,120 @@
angular.module('DashboardGraphs')
.directive('jobStatusGraph', ['$rootScope', '$compile', '$location' , '$window', 'Wait', 'adjustGraphSize', 'jobStatusGraphData',
function ($rootScope, $compile , $location, $window, Wait, adjustGraphSize) {
return {
restrict: 'E',
templateUrl: '/static/partials/job_status_graph.html',
link: link
};
function link(scope, element, attr) {
var job_type, job_status_chart = nv.models.lineChart();
scope.period="month";
scope.jobType="all";
scope.$watch(attr.data, function(value) {
if (value) {
createGraph(value, scope.period, scope.jobType);
}
});
function createGraph(data, period, jobtype){
scope.period = period;
scope.jobType = jobtype;
var timeFormat, graphData = [
{ "color": "#00aa00",
"key": "Successful",
"values": data.jobs.successful
},
{ "key" : "Failed" ,
"color" : "#aa0000",
"values": data.jobs.failed
}
];
if(period==="day") {
timeFormat="%H:%M";
}
else {
timeFormat = '%m/%d';
}
graphData.map(function(series) {
series.values = series.values.map(function(d) {
return {
x: d[0],
y: d[1]
};
});
return series;
});
job_status_chart
.margin({top: 5, right: 75, bottom: 40, left: 85}) //Adjust chart margins to give the x-axis some breathing room.
.x(function(d,i) { return i; })
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
.showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showYAxis(true) //Show the y-axis
.showXAxis(true); //Show the x-axis
job_status_chart.xAxis
.axisLabel("Time")//.showMaxMin(true)
.tickFormat(function(d) {
var dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
return dx ? d3.time.format(timeFormat)(new Date(Number(dx+'000'))) : '';
});
job_status_chart.yAxis //Chart y-axis settings
.axisLabel('Jobs')
.tickFormat(d3.format('.f'));
d3.select(element.find('svg')[0])
.datum(graphData)
.call(job_status_chart)
.style({
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/fonts/OpenSans-Regular.ttf)"
});
// when the Period drop down filter is used, create a new graph based on the
d3.selectAll(element.find(".n"))
.on("click", function() {
period = this.getAttribute("id");
$('#period-dropdown').replaceWith("<a id=\"period-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+"<span class=\"caret\"><span>\n");
createGraph(data, period, job_type);
});
//On click, update with new data
d3.selectAll(element.find(".m"))
.on("click", function() {
job_type = this.getAttribute("id");
$('#type-dropdown').replaceWith("<a id=\"type-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+"<span class=\"caret\"><span>\n");
createGraph(data, period, job_type);
});
adjustGraphSize(job_status_chart, element);
}
function onResize() {
adjustGraphSize(job_status_chart, element);
}
angular.element($window).on('resize', onResize);
element.on('$destroy', function() {
angular.element($window).off('resize', onResize);
});
if (scope.removeGraphDataReady) {
scope.removeGraphDataReady();
}
}
}]);

View File

@ -1074,8 +1074,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
msg: 'Failed to retrieve inventory source. GET status: ' + status });
});
}
else
else {
modal_scope.$emit('groupVariablesLoaded'); // JT-- "groupVariablesLoaded" is where the schedule info is loaded, so I make a call after the sources_scope.source has been loaded
}
});
if (sources_scope.removeScopeSourceTypeOptionsReady) {

View File

@ -1208,10 +1208,12 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge
keys;
function listSort(a,b) {
if (parseInt(a,10) < parseInt(b,10))
if (parseInt(a,10) < parseInt(b,10)) {
return -1;
if (parseInt(a,10) > parseInt(b,10))
}
if (parseInt(a,10) > parseInt(b,10)) {
return 1;
}
return 0;
}
@ -1276,10 +1278,12 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge
idx, key, keys, newKeys, tasks, t;
function listSort(a,b) {
if (parseInt(a,10) < parseInt(b,10))
if (parseInt(a,10) < parseInt(b,10)) {
return -1;
if (parseInt(a,10) > parseInt(b,10))
}
if (parseInt(a,10) > parseInt(b,10)) {
return 1;
}
return 0;
}
@ -1385,15 +1389,19 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge
keys = Object.keys(filteredListB);
keys.sort(function compare(a, b) {
if (filteredListB[a].name === filteredListB[b].name) {
if (filteredListB[a].counter < filteredListB[b].counter)
if (filteredListB[a].counter < filteredListB[b].counter) {
return -1;
if (filteredListB[a].counter >filteredListB[b].counter)
}
if (filteredListB[a].counter >filteredListB[b].counter) {
return 1;
}
} else {
if (filteredListB[a].name < filteredListB[b].name)
if (filteredListB[a].name < filteredListB[b].name) {
return -1;
if (filteredListB[a].name > filteredListB[b].name)
}
if (filteredListB[a].name > filteredListB[b].name) {
return 1;
}
}
// a must be equal to b
return 0;
@ -1453,10 +1461,12 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge
keys = Object.keys(filteredListB);
keys.sort(function(a,b) {
if (filteredListB[a].name > filteredListB[b].name)
if (filteredListB[a].name > filteredListB[b].name) {
return 1;
if (filteredListB[a].name < filteredListB[b].name)
}
if (filteredListB[a].name < filteredListB[b].name) {
return -1;
}
// a must be equal to b
return 0;
});

View File

@ -111,7 +111,9 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential
if(scope.prompt_for_vars===false && scope.survey_enabled===true){
scope.$emit('GetExtraVars');
}
else scope.$emit('BuildData');
else {
scope.$emit('BuildData');
}
};
@ -831,7 +833,9 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi
else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) {
scope.$emit('PromptForSurvey', html, url);
}
else scope.$emit('StartPlaybookRun', url);
else {
scope.$emit('StartPlaybookRun', url);
}
}
})
@ -1021,4 +1025,4 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi
});
};
}
]);
]);

View File

@ -95,7 +95,9 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job
if(scope.$parent.portalMode===true){
$window.open('/#/jobs/' + job.id, '_blank');
}
else $location.url('/jobs/' + job.id);
else {
$location.url('/jobs/' + job.id);
}
}
else {
LogViewer({

View File

@ -34,8 +34,8 @@ angular.module('PermissionsHelper', [])
} else {
scope.projectrequired = true;
html = "<dl>\n" +
"<dt>Create</dt>\n" +
"<dd>Allow the user or team to create job templates. This implies that they have the Run and Check permissions.</dd>\n" +
"<dt>Create</dt>\n" +
"<dd>Allow the user or team to create job templates. This implies that they have the Run and Check permissions.</dd>\n" +
"<dt>Run</dt>\n" +
"<dd>Allow the user or team to run a job template from the project against the inventory. In Run mode modules will " +
"be executed, and changes to the inventory will occur.</dd>\n" +

View File

@ -73,7 +73,9 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper',
if(scope.can_edit === false){
$('#survey-save-button').attr('disabled', "disabled");
}
else $('#survey-save-button').attr('ng-disabled', "survey_questions.length<1 ");
else {
$('#survey-save-button').attr('ng-disabled', "survey_questions.length<1 ");
}
element = angular.element(document.getElementById('survey-save-button'));
$compile(element)(scope);
@ -513,7 +515,9 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper',
if(scope.mode === 'add'){
questions = [];
}
else scope.survey_questions = [];
else {
scope.survey_questions = [];
}
$(me).dialog('close');
};

View File

@ -0,0 +1,90 @@
angular.module('DashboardGraphs').
factory('adjustGraphSize', function() {
// Adjusts the size of graphs based on the current height
// of the outer parent (see auto-size-module directive).
//
// Since the graph's svg element is set to width & height of 100%,
// it will automatically size itself when the size of its container
// changes. Since boxes in HTML automatically fill the width of their
// parent, we don't have to change the container's width. However,
// since the makers HTML never heard of vertical rhythm,
// we have to manually set a new height on the container.
//
// ## Calculating the container's new height
//
// newHeight is the height we assign to the graph's immediate parent.
// This is calculated as the height of the graph-container (the
// outer parent), offset by the height of the toolbar row
// (the contains the title and/or any filters) and the
// bottom margin.
//
// ## Responsive Graph Stuff
//
// Letting the svg element automatically scale only solves part of
// the responsive graph problem. d3 draws graphs as paths, with static
// positioning of all elements. Therefore, we need to tell the graph how
// to adjust itself so that it can resize properly.
//
// ### Resizing the axes
//
// First we get the width & height of the chart after it has been modified
// by setting the height on its parent (see Calculating the New Container's
// Height above). Note that we need to offset the width/height by the margins
// to make sure we keep all the spacing intact.
//
// Next, we update the range for x & y to take the new width & height into
// account. d3 uses this range to map domain values (the actual data) onto
// pixels.
//
// After that we adjust the number of ticks on the axes. This makes sure we
// will never have overlapping ticks. If that does become a problem, try
// changing the divisor in the calculations to a different number until you
// find something that helps. For example, (width / 75) should make the x
// axis only ever display 1 tick per every 75 pixels.
//
// ### Redrawing the line
//
// Since this is a line graph, now that we've changed the range & ticks,
// we need to instruct d3 to repaint (redraw) the actual lines representing
// the data. We do this by setting the "d" attribute of the path element
// that represents the line to the line function on the chart model. This
// function triggers the mapping of domain to range, and plots the chart.
// Calling chartModel.update() at the end instructs nv to process our changes.
//
return function adjustGraphSize(chartModel, element) {
var parentHeight = element.parent().parent().height();
var toolbarHeight = element.find('.toolbar').height();
var container = element.find('svg').parent();
var margins = chartModel.margin();
var newHeight = parentHeight - toolbarHeight - margins.bottom;
$(container).height(newHeight);
var graph = d3.select(element.find('svg')[0]);
var width = parseInt(graph.style('width')) - margins.left - margins.right;
var height = parseInt(graph.style('height')) - margins.top - margins.bottom;
chartModel.xRange([0, width]);
chartModel.yRange([height, 0]);
chartModel.xAxis.ticks(Math.max(width / 75, 2));
chartModel.yAxis.ticks(Math.max(height / 50, 2));
if (height < 160) {
graph.select('.y.nv-axis').select('.domain').style('display', 'none');
graph.select('.y.nv-axis').select('.domain').style('display', 'initial');
}
graph.select('.x.nv-axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(chartModel.xAxis);
graph.selectAll('.line')
.attr('d', chartModel.lines);
chartModel.update();
};
});

View File

@ -0,0 +1 @@
angular.module('DataServices', []);

View File

@ -0,0 +1,47 @@
angular.module('DataServices')
.service('hostCountGraphData',
["Rest",
"GetBasePath",
"ProcessErrors",
"$q",
HostCountGraphData]);
function HostCountGraphData(Rest, getBasePath, processErrors, $q) {
function pluck(property, promise) {
return promise.then(function(value) {
return value[property];
});
}
function getLicenseData() {
var url = getBasePath('config');
Rest.setUrl(url);
return Rest.get()
.then(function (data){
var license = data.data.license_info.instance_count;
return license;
});
}
function getHostData() {
var url = getBasePath('dashboard')+'graphs/inventory/';
Rest.setUrl(url);
return pluck('data', Rest.get());
}
return {
get: function() {
return $q.all({
license: getLicenseData(),
hosts: getHostData()
}).catch(function (response) {
var errorMessage = 'Failed to get: ' + response.url + ' GET returned: ' + response.status;
processErrors(null, response.data, response.status, null, { hdr: 'Error!',
msg: errorMessage
});
return response;
});
}
};
}

View File

@ -0,0 +1,59 @@
angular.module('DataServices')
.service('jobStatusGraphData',
["Rest",
"GetBasePath",
"ProcessErrors",
"$rootScope",
JobStatusGraphData]);
function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope) {
function pluck(property, promise) {
return promise.then(function(value) {
return value[property];
});
}
function getData(period, jobType) {
var url = getBasePath('dashboard')+'graphs/jobs/?period='+period+'&job_type='+jobType;
Rest.setUrl(url);
var result = Rest.get()
.catch(function(response) {
var errorMessage = 'Failed to get: ' + response.url + ' GET returned: ' + response.status;
processErrors(null,
response.data,
response.status,
null, {
hdr: 'Error!',
msg: errorMessage
});
return response;
});
return pluck('data', result);
}
return {
destroyWatcher: angular.noop,
setupWatcher: function(period, jobType) {
this.destroyWatcher =
$rootScope.$on('JobStatusChange', function() {
getData(period, jobType).then(function(result) {
$rootScope.
$broadcast('DataReceived:JobStatusGraph',
result);
return result;
});
});
},
get: function(period, jobType) {
this.destroyWatcher();
this.setupWatcher(period, jobType);
return getData(period, jobType);
}
};
}

View File

@ -1,180 +0,0 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*/
/**
* @ngdoc function
* @name widgets.function:HostGraph
* @description
*
*/
'use strict';
angular.module('HostGraphWidget', ['RestServices', 'Utilities'])
.factory('HostGraph', ['$rootScope', '$compile', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait',
function ($rootScope, $compile, $location, Rest, GetBasePath, ProcessErrors) {
return function (params) {
var scope = params.scope,
target = params.target,
html, element, url, license, license_graph;
// html = "<div class=\"graph-container\">\n";
html ="<div class=\"row\">\n";
html += "<div class=\"h6 col-xs-8 text-center\"><b>Host Count</b></div>\n";
html += "</div>\n";
html +="<div class=\"row\">\n";
html += "<div class=\"host-count-graph\"><svg></svg></div>\n";
// html += "</div>\n";
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
url = GetBasePath('config');
if (scope.removeResizeHostGraph) {
scope.removeResizeHostGraph();
}
scope.removeResizeHostGraph= scope.$on('ResizeHostGraph', function () {
if($(window).width()<500){
$('.graph-container').height(300);
}
else{
var winHeight = $(window).height(),
available_height = winHeight - $('#main-menu-container .navbar').outerHeight() - $('#count-container').outerHeight() - 120;
$('.graph-container').height(available_height/2);
license_graph.update();
}
});
Rest.setUrl(url);
Rest.get()
.success(function (data){
license = data.license_info.instance_count;
scope.$emit('licenseCountReady', license);
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get: ' + url + ' GET returned: ' + status });
});
if (scope.removeLicenseCountReady) {
scope.removeLicenseCountReady();
}
scope.removeLicenseCountReady = scope.$on('licenseCountReady', function (e, license) {
url = GetBasePath('dashboard')+'graphs/inventory/';
Rest.setUrl(url);
Rest.get()
.success(function (data) {
scope.$emit('hostDataReady', data, license);
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get: ' + url + ' GET returned: ' + status });
});
});
if (scope.removeHostDataReady) {
scope.removeHostDataReady();
}
scope.removeHostDataReady = scope.$on('hostDataReady', function (e, data, license) {
//url = GetBasePath('dashboard')+'graphs/';
var graphData = [
{
"key" : "Hosts" ,
"color" : "#1778c3",
"values": data.hosts
},
{
"key" : "License" ,
"color" : "#171717",
"values": data.hosts
}
];
graphData.map(function(series) {
if(series.key==="Hosts"){
series.values = series.values.map(function(d) {
return {
x: d[0],
y: d[1]
};
});
}
if(series.key==="License"){
series.values = series.values.map(function(d) {
return {
x: d[0],
y: license
};
});
}
return series;
});
nv.addGraph({
generate: function() {
var width = $('.graph-container').width(), // nv.utils.windowSize().width/3,
height = $('.graph-container').height()*0.6; //nv.utils.windowSize().height/5,
license_graph = nv.models.lineChart()
.margin({top: 15, right: 75, bottom: 40, left: 85})
.x(function(d,i) { return i ;})
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
.transitionDuration(350) //how fast do you want the lines to transition?
.showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showYAxis(true) //Show the y-axis
.showXAxis(true) //Show the x-axis
;
license_graph.xAxis
.axisLabel("Time")
.tickFormat(function(d) {
var dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
return dx ? d3.time.format('%m/%d')(new Date(Number(dx+'000'))) : '';
});
license_graph.yAxis //Chart y-axis settings
.axisLabel('Hosts')
.tickFormat(d3.format('.f'));
d3.select('.host-count-graph svg')
.datum(graphData).transition()
.attr('width', width)
.attr('height', height)
.duration(500)
.call(license_graph)
.style({
// 'width': width,
// 'height': height,
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/fonts/OpenSans-Regular.ttf)"
});
// nv.utils.windowResize(license_graph.update);
scope.$emit('WidgetLoaded');
return license_graph;
},
});
//});
});
};
}
]);

View File

@ -1,128 +0,0 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*/
/**
* @ngdoc function
* @name widgets.function:HostPieChart
* @description
* HostPieChart.js
*
* file for the host status pie chart
*
*/
'use strict';
angular.module('HostPieChartWidget', ['RestServices', 'Utilities'])
.factory('HostPieChart', ['$rootScope', '$compile',
//'Rest', 'GetBasePath', 'ProcessErrors', 'Wait',
function ($rootScope, $compile){
//, Rest, GetBasePath, ProcessErrors) {
return function (params) {
var scope = params.scope,
target = params.target,
dashboard = params.dashboard,
html, element, data,
canvas, context, winHeight, available_height, host_pie_chart;
// html = "<div class=\"graph-container\">\n";
html ="<div class=\"row\">\n";
html += "<div id=\"job-status-title\" class=\"h6 col-xs-8 text-center\"><b>Host Status</b></div>\n";
html += "</div>\n";
html +="<div class=\"row\">\n";
html += "<div class=\"host-pie-chart text-center\"><svg></svg></div>\n";
html += "</div>\n";
// html += "</div>\n";
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
if (scope.removeResizeHostPieGraph) {
scope.removeResizeHostPieGraph();
}
scope.removeResizeHostPieGraph= scope.$on('ResizeHostPieGraph', function () {
if($(window).width()<500){
$('.graph-container').height(300);
}
else{
var winHeight = $(window).height(),
available_height = winHeight - $('#main-menu-container .navbar').outerHeight() - $('#count-container').outerHeight() - 120;
$('.graph-container').height(available_height/2);
if(host_pie_chart){
host_pie_chart.update();
}
}
});
if(dashboard.hosts.total+dashboard.hosts.failed>0){
data = [
{
"label": "Successful",
"color": "#00aa00",
"value" : dashboard.hosts.total
} ,
{
"label": "Failed",
"color" : "#aa0000",
"value" : dashboard.hosts.failed
}
];
nv.addGraph(function() {
var width = $('.graph-container').width(), // nv.utils.windowSize().width/3,
height = $('.graph-container').height()*0.7; //nv.utils.windowSize().height/5,
host_pie_chart = nv.models.pieChart()
.margin({top: 5, right: 75, bottom: 40, left: 85})
.x(function(d) { return d.label; })
.y(function(d) { return d.value; })
.showLabels(true)
.labelThreshold(0.01)
.tooltipContent(function(x, y) {
return '<b>'+x+'</b>'+ '<p>' + Math.floor(y.replace(',','')) + ' Hosts ' + '</p>';
})
.color(['#00aa00', '#aa0000']);
host_pie_chart.pie.pieLabelsOutside(true).labelType("percent");
d3.select(".host-pie-chart svg")
.datum(data)
.attr('width', width)
.attr('height', height)
.transition().duration(350)
.call(host_pie_chart)
.style({
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/fonts/OpenSans-Regular.ttf)"
});
// nv.utils.windowResize(host_pie_chart.update);
scope.$emit('WidgetLoaded');
return host_pie_chart;
});
}
else{
winHeight = $(window).height();
available_height = winHeight - $('#main-menu-container .navbar').outerHeight() - $('#count-container').outerHeight() - 120;
$('.graph-container:eq(1)').height(available_height/2);
$('.host-pie-chart svg').replaceWith('<canvas id="circlecanvas" width="120" height="120"></canvas>');
canvas = document.getElementById("circlecanvas");
context = canvas.getContext("2d");
context.arc(55, 55, 50, 0, Math.PI * 2, false);
context.lineWidth = 1;
context.strokeStyle = '#1778c3';
context.stroke();
context.font = "12px Open Sans";
context.fillText("No Host data",18,55);
scope.$emit('WidgetLoaded');
}
};
}
]);

View File

@ -1,219 +0,0 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*/
/**
* @ngdoc function
* @name widgets.function:JobStatusGraph
* @description
*/
'use strict';
angular.module('JobStatusGraphWidget', ['RestServices', 'Utilities'])
.factory('JobStatusGraph', ['$rootScope', '$compile', '$location' , 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait',
function ($rootScope, $compile , $location, Rest, GetBasePath, ProcessErrors) {
return function (params) {
var scope = params.scope,
target = params.target,
// dashboard = params.dashboard,
html, element, url, job_status_chart,
period="month",
job_type="all";
// html = "<div class=\"graph-container\">\n";
html = "<div class=\"row\">\n";
html += "<div id=\"job-status-title\" class=\"h6 col-xs-2 col-sm-3 col-lg-4 text-center\"><b>Job Status</b></div>\n"; // for All Jobs, Past Month
html += "<div class=\"h6 col-xs-5 col-sm-5 col-lg-4\">\n";
html += "<div class=\"dropdown\">\n";
html += "Job Type: <a id=\"type-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">\n";
html += "All<span class=\"caret\"></span>\n";
html += " </a>\n";
html += "<ul class=\"dropdown-menu\" role=\"menu\" aria-labelledby=\"type-dropdown\">\n";
html += "<li><a class=\"m\" id=\"all\">All</a></li>\n";
html += "<li><a class=\"m\" id=\"inv_sync\">Inventory Sync</a></li>\n";
html += "<li><a class=\"m\" id=\"scm_update\">SCM Update</a></li>\n";
html += "<li><a class=\"m\" id=\"playbook_run\">Playbook Run</a></li>\n";
html += "</ul>\n";
html += "</div>\n";
html += "</div>\n"; //end of filter div
html += "<div class=\"h6 col-xs-5 col-sm-4 col-lg-4\">\n";
html += "<div class=\"dropdown\">\n";
html += "Period: <a id=\"period-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">\n";
html += "Past Month<span class=\"caret\"></span>\n";
html += " </a>\n";
html += "<ul class=\"dropdown-menu\" role=\"menu\" aria-labelledby=\"period-dropdown\">\n";
html += "<li><a class=\"n\" id=\"day\" >Past 24 Hours </a></li>\n";
html += "<li><a class=\"n\" id=\"week\">Past Week</a></li>\n";
html += "<li><a class=\"n\" id=\"month\">Past Month</a></li>\n";
html += "</ul>\n";
html += "</div>\n";
html += "</div>\n"; //end of filter div
html += "</div>\n"; // end of row
html +="<div class=\"row\">\n";
html += "<div class=\"job-status-graph\"><svg></svg></div>\n";
html += "</div>\n";
// html += "</div>\n";
function createGraph(){
url = GetBasePath('dashboard')+'graphs/jobs/?period='+period+'&job_type='+job_type;
Rest.setUrl(url);
Rest.get()
.success(function (data){
scope.$emit('graphDataReady', data);
return job_type, period;
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get: ' + url + ' GET returned: ' + status });
});
}
if ($rootScope.removeReloadJobStatusGraph) {
$rootScope.removeReloadJobStatusGraph();
}
$rootScope.removeReloadJobStatusGraph = $rootScope.$on('ReloadJobStatusGraph', function() {
createGraph();
});
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
createGraph();
if (scope.removeResizeJobGraph) {
scope.removeResizeJobGraph();
}
scope.removeResizeJobGraph= scope.$on('ResizeJobGraph', function () {
if($(window).width()<500){
$('.graph-container').height(300);
}
else{
var winHeight = $(window).height(),
available_height = winHeight - $('#main-menu-container .navbar').outerHeight() - $('#count-container').outerHeight() - 120;
$('.graph-container').height(available_height/2);
job_status_chart.update();
}
});
if (scope.removeGraphDataReady) {
scope.removeGraphDataReady();
}
scope.removeGraphDataReady = scope.$on('graphDataReady', function (e, data) {
var timeFormat, graphData = [
{
"color": "#00aa00",
"key": "Successful",
"values": data.jobs.successful
},
{
"key" : "Failed" ,
"color" : "#aa0000",
"values": data.jobs.failed
}
];
if(period==="day"){
timeFormat="%H:%M";
}
else {
timeFormat = '%m/%d';
}
graphData.map(function(series) {
series.values = series.values.map(function(d) {
return {
x: d[0],
y: d[1]
};
});
return series;
});
nv.addGraph({
generate: function() {
var width = $('.graph-container').width(), // nv.utils.windowSize().width/3,
height = $('.graph-container').height()*0.7; //nv.utils.windowSize().height/5,
job_status_chart = nv.models.lineChart()
.margin({top: 5, right: 75, bottom: 80, left: 85}) //Adjust chart margins to give the x-axis some breathing room.
.x(function(d,i) { return i; })
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
.transitionDuration(350) //how fast do you want the lines to transition?
.showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showYAxis(true) //Show the y-axis
.showXAxis(true) //Show the x-axis
// .width(width)
// .height(height)
;
job_status_chart.xAxis
.axisLabel("Time")//.showMaxMin(true)
.tickFormat(function(d) {
var dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
return dx ? d3.time.format(timeFormat)(new Date(Number(dx+'000'))) : '';
});
job_status_chart.yAxis //Chart y-axis settings
.axisLabel('Jobs')
.tickFormat(d3.format('.f'));
d3.select('.job-status-graph svg')
.datum(graphData).transition()
.attr('width', width)
.attr('height', height)
.duration(1000)
.call(job_status_chart)
.style({
// 'width': width,
// 'height': height,
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/fonts/OpenSans-Regular.ttf)"
});
// when the Period drop down filter is used, create a new graph based on the
d3.selectAll(".n")
.on("click", function() {
period = this.getAttribute("id");
$('#period-dropdown').replaceWith("<a id=\"period-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+"<span class=\"caret\"><span>\n");
createGraph();
});
//On click, update with new data
d3.selectAll(".m")
.on("click", function() {
job_type = this.getAttribute("id");
$('#type-dropdown').replaceWith("<a id=\"type-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+"<span class=\"caret\"><span>\n");
createGraph();
});
scope.$emit('WidgetLoaded');
return job_status_chart;
},
});
});
};
}
]);

View File

@ -58,7 +58,7 @@
@import "breadcrumbs.less";
@import "stdout.less";
@import "lists.less";
@import "new-dashboard.less";
@import "dashboard.less";
@import "jPushMenu.less";
@import "survey-maker.less";
@import "portal.less";

View File

@ -1,13 +1,26 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*
* new-dashboard.css
* dashboard.css
*
* custom styles for the new dashboard
*
*/
.graph-wrapper {
width: 100%;
}
.graph {
background-color: white;
// @include transition(width 2s ease-in-out, height 2s ease-in-out);
position: relative;
text-align: center;
width: 100%;
height: 100%;
margin: 0 auto;
}
.job-status-graph, .host-count-graph{
font: 10px sans-serif;
@ -99,4 +112,4 @@ due to the login screen showing on top of the dashboard, we're hiding the border
.m, .n{
cursor:pointer;
}
}

View File

@ -49,7 +49,6 @@ angular.module('Utilities', ['RestServices', 'Utilities'])
} catch (e) {
// ignore
}
$(window).unbind('resize');
};
}])
@ -859,4 +858,4 @@ angular.module('Utilities', ['RestServices', 'Utilities'])
};
}
]);
]);

View File

@ -100,8 +100,9 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
scope.update = function(){
var val = [];
angular.forEach(scope.cbModel, function(v,k){
if (v)
if (v) {
val.push(k);
}
});
if (val.length>0){
scope.ngModel.value = val;

View File

@ -223,8 +223,6 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'Utilities', 'ListGenerator
$(this).remove();
});
$(window).unbind('resize');
// Prepend an asterisk to required field label
$('.form-control[required], input[type="radio"][required]').each(function () {
var label, span;
@ -1656,4 +1654,4 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'Utilities', 'ListGenerator
}
};
}
]);
]);

View File

@ -186,7 +186,6 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
$(this).remove();
});
$(window).unbind('resize');
try {
$('#help-modal').empty().dialog('destroy');
@ -426,10 +425,11 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
}
else // its assumed that options.input_type = checkbox
else { // its assumed that options.input_type = checkbox
html += "<td><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-true-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
}
} else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) {
// Row level actions
@ -582,4 +582,4 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
return html;
}
};
}]);
}]);

View File

@ -63,14 +63,16 @@ angular.module('PromptDialog', ['Utilities'])
focus = function() {
var focusableElement = focusableChildren[currentIndex];
if (focusableElement)
if (focusableElement) {
focusableElement.focus();
}
};
focusPrevious = function () {
currentIndex--;
if (currentIndex < 0)
if (currentIndex < 0) {
currentIndex = numElements - 1;
}
focus();
@ -79,8 +81,9 @@ angular.module('PromptDialog', ['Utilities'])
focusNext = function () {
currentIndex++;
if (currentIndex >= numElements)
if (currentIndex >= numElements) {
currentIndex = 0;
}
focus();
@ -118,4 +121,4 @@ angular.module('PromptDialog', ['Utilities'])
};
}
]);
]);

View File

@ -1,5 +1,5 @@
<div class="tab-pane" id="home">
<div class="tab-pane" id="home" ng-if="current_user">
<div ng-cloak id="htmlTemplate" style="padding:10px">
<div id="refresh-row" class="row">
<div class="col-lg-12">
@ -12,12 +12,24 @@
<div id="dash-counts" class="col-sm-12 col-xs-12"></div>
</div>
<div class="row">
<div class="left-side col-sm-6 col-xs-12"><div id="dash-job-status-graph" class="graph-container"></div></div>
<div class="right-side col-sm-6 col-xs-12"><div id="dash-host-status-graph" class="graph-container"></div></div>
<div class="left-side col-sm-6 col-xs-12">
<div id="dash-job-status-graph" auto-size-module class="graph-container">
<job-status-graph data="graphData.jobStatus" period="month" job-type="all"></job-status-graph>
</div>
</div>
<div class="right-side col-sm-6 col-xs-12">
<div id="dash-host-status-graph" auto-size-module class="graph-container">
<host-status-graph data="dashboardData"></host-status-graph>
</div>
</div>
</div>
<div class="row">
<div id="dash-jobs-list" class="left-side col-sm-6 col-xs-12"></div>
<div class="right-side col-sm-6 col-xs-12"><div id="dash-host-count-graph" class="graph-container"></div></div>
<div class="right-side col-sm-6 col-xs-12">
<div id="dash-host-count-graph" auto-size-module class="graph-container">
<host-count-graph ng-if="user_is_superuser" data="graphData.hostCounts"></host-count-graph>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div class="graph-wrapper">
<div class="clearfix toolbar">
<div class="h6 pull-left">
<b>Host Count</b>
</div>
</div>
<div class="graph">
<svg width="100%" height="100%"></svg>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div class="graph-wrapper">
<div class="clearfix toolbar">
<div id="job-status-title" class="h6 pull-left">
<b>Host Status</b>
</div>
</div>
<div class="graph">
<svg width="100%" height="100%" preserveAspectRatio="xMinYMin"></svg>
</div>
</div>

View File

@ -0,0 +1,39 @@
<div class="graph-wrapper job-status-graph">
<div class="clearfix toolbar">
<div id="job-status-title" class="h6 pull-left">
<b>Job Status</b>
</div>
<div class="h6 dropdown pull-right">
Period: <a id="period-dropdown" role="button" data-toggle="dropdown" data-target="#" href="/page.html">
Past Month<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="period-dropdown">
<li><a class="n" id="day" >Past 24 Hours </a></li>
<li><a class="n" id="week">Past Week</a></li>
<li><a class="n" id="month">Past Month</a></li>
</ul>
</div>
<div class="h6 dropdown pull-right" style="padding-right: .5em;">
Job Type: <a id="type-dropdown" role="button" data-toggle="dropdown" data-target="#" href="/page.html">
All<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="type-dropdown">
<li><a class="m" id="all">All</a></li>
<li><a class="m" id="inv_sync">Inventory Sync</a></li>
<li><a class="m" id="scm_update">SCM Update</a></li>
<li><a class="m" id="playbook_run">Playbook Run</a></li>
</ul>
</div>
</div>
<div class="graph">
<svg width="100%" height="100%"></svg>
</div>
</div>

View File

@ -81,6 +81,15 @@
<script src="{{ STATIC_URL }}js/controllers/Permissions.js"></script>
<script src="{{ STATIC_URL }}js/controllers/Schedules.js"></script>
<script src="{{ STATIC_URL }}js/controllers/Sockets.js"></script>
<script src="{{ STATIC_URL }}js/services/data-services.js"></script>
<script src="{{ STATIC_URL }}js/services/host-count-graph-data.js"></script>
<script src="{{ STATIC_URL }}js/services/job-status-graph-data.js"></script>
<script src="{{ STATIC_URL }}js/directives/dashboard-graphs.js"></script>
<script src="{{ STATIC_URL }}js/services/adjust-graph-size.js"></script>
<script src="{{ STATIC_URL }}js/directives/auto-size-module.js"></script>
<script src="{{ STATIC_URL }}js/directives/job-status-graph.js"></script>
<script src="{{ STATIC_URL }}js/directives/host-status-graph.js"></script>
<script src="{{ STATIC_URL }}js/directives/host-count-graph.js"></script>
<script src="{{ STATIC_URL }}js/forms/Users.js"></script>
<script src="{{ STATIC_URL }}js/forms/Organizations.js"></script>
<script src="{{ STATIC_URL }}js/forms/Inventories.js"></script>
@ -170,9 +179,6 @@
<script src="{{ STATIC_URL }}js/helpers/AboutAnsible.js"></script>
<script src="{{ STATIC_URL }}js/widgets/JobStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/DashboardCounts.js"></script>
<script src="{{ STATIC_URL }}js/widgets/HostPieChart.js"></script>
<script src="{{ STATIC_URL }}js/widgets/HostGraph.js"></script>
<script src="{{ STATIC_URL }}js/widgets/JobStatusGraph.js"></script>
<script src="{{ STATIC_URL }}js/widgets/DashboardJobs.js"></script>
<script src="{{ STATIC_URL }}js/widgets/PortalJobs.js"></script>
<script src="{{ STATIC_URL }}js/widgets/Stream.js"></script>

View File

@ -10,7 +10,8 @@ module.exports = function(config) {
conf.files = conf.files.concat([
'../static/lib/angular-mocks/angular-mocks.js',
'../../../node_modules/ng-midway-tester/src/ngMidwayTester.js',
'./unit/*'
'./unit/*',
'./unit/**/*'
]);
// level of logging

View File

@ -1,23 +1,23 @@
// Karma configuration
// Generated on Mon Aug 04 2014 21:17:04 GMT-0400 (EDT)
module.exports = function() {
return {
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
frameworks: ['mocha', 'chai', 'sinon-chai', 'chai-as-promised'],
// list of files / patterns to load in the browser
files: [
'../static/lib/jquery/dist/jquery.min.js',
'../static/lib/angular/angular.min.js',
'../static/lib/angular-route/angular-route.min.js',
'../static/lib/angular-resource/angular-resource.min.js',
'../static/lib/angular-cookies/angular-cookies.min.js',
'../static/lib/angular/angular.js',
'../static/lib/angular-route/angular-route.js',
'../static/lib/angular-resource/angular-resource.js',
'../static/lib/angular-cookies/angular-cookies.js',
'../static/lib/angular-sanitize/angular-sanitize.min.js',
'../static/lib/angular-md5/angular-md5.min.js',
'../static/lib/angular-codemirror/lib/AngularCodeMirror.js',
@ -30,9 +30,8 @@ module.exports = function() {
'../static/lib/angular-scheduler/lib/angular-scheduler.min.js',
'../static/lib/jqueryui/ui/minified/jquery-ui.min.js',
'../static/lib/bootstrap/dist/js/bootstrap.min.js',
'../static/lib/js-yaml/js-yaml.min.js',
'../static/lib/js-yaml/dist/js-yaml.min.js',
'../static/lib/select2/select2.min.js',
'../static/lib/js-yaml/js-yaml.min.js',
'../static/lib/jsonlint/lib/jsonlint.js',
'../static/lib/codemirror/lib/codemirror.js',
'../static/lib/codemirror/mode/javascript/javascript.js',
@ -52,8 +51,13 @@ module.exports = function() {
'../static/lib/lrInfiniteScroll/lrInfiniteScroll.js',
'../static/lib/ansible/*.js',
'../static/js/config.js',
'../static/js/directives/dashboard-graphs.js',
'../static/js/*/*.js',
'../static/js/app.js'
'../static/js/app.js',
'../static/lib/angular-mocks/angular-mocks.js',
'../../../node_modules/ng-midway-tester/src/ngMidwayTester.js',
'./unit/*',
'./unit/**/*'
],
@ -72,7 +76,13 @@ module.exports = function() {
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
reporters: ['dots', 'progress'],
client: {
mocha: {
ui: 'bdd'
}
},
// web server port
@ -84,7 +94,7 @@ module.exports = function() {
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
autoWatch: true,
// start these browsers
@ -96,5 +106,5 @@ module.exports = function() {
// if true, Karma captures browsers, runs the tests and exits
singleRun: false
};
});
};

View File

@ -1,183 +0,0 @@
/**********************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*
* CheckLicense.js
*
* Tests the CheckLicense service- helpers/CheckLicense.js
*
*/
/* global describe, it, beforeEach, expect, module, inject */
var licenses = [{
desc: 'expired license with < 1 day grace period',
valid_key: true,
time_remaining: 0,
grace_period_remaining: 85000,
free_instances: 10,
expects: 'grace period has been exceeded'
}, {
desc: 'expired license with > 1 day grace period',
valid_key: true,
time_remaining: 0,
grace_period_remaining: (86400 * 2),
free_instances: 10,
expects: '2 grace days'
}, {
desc: 'valid license with time remaining = 15 days',
valid_key: true,
time_remaining: (86400 * 15),
grace_period_remaining: 0,
free_instances: 10,
expects: 'license is valid'
}, {
desc: 'valid license with time remaining < 15 days',
valid_key: true,
time_remaining: (86400 * 10) ,
grace_period_remaining: 0,
free_instances: 10,
expects: 'license has 10 days remaining'
}, {
desc: 'valid license with time remaining > 15 days and remaining hosts > 0',
valid_key: true,
time_remaining: (86400 * 20),
free_instances: 10,
grace_period_remaining: 0,
expects: 'license is valid'
}, {
desc: 'valid license with time remaining > 15 days and remaining hosts = 0',
valid_key: true,
time_remaining: (86400 * 20) ,
grace_period_remaining: 0,
free_instances: 0,
expects: 'license has reached capacity'
}, {
desc: 'expired trial license with > 1 day grace period',
valid_key: true,
trial: true,
time_remaining: 0,
grace_period_remaining: (86400 * 2),
free_instances: 10,
notExpects: 'grace days'
} , {
desc: 'expired trial license with < 1 day grace period',
valid_key: true,
trial: true,
time_remaining: 0,
grace_period_remaining: 0,
free_instances: 10,
notExpects: '30 day grace period'
}, {
desc: 'trial license with time remaining = 15 days',
trial: true,
valid_key: true,
time_remaining: (86400 * 15),
grace_period_remaining: 0,
free_instances: 10,
notExpects: 'grace period'
}, {
desc: 'trial license with time remaining < 15 days',
valid_key: true,
trial: true,
time_remaining: (86400 * 10) ,
grace_period_remaining: 0,
free_instances: 10,
notExpects: 'grace period'
}];
var should_notify = [{
desc: 'should notify when license expired',
valid_key: true,
time_remaining: 0,
grace_period_remaining: 85000,
free_instances: 10
}, {
desc: 'should notify when license time remaining < 15 days',
valid_key: true,
time_remaining: (86400 * 10) ,
grace_period_remaining: 0,
free_instances: 10
}, {
desc: 'should notify when host count <= 0',
valid_key: true,
time_remaining: (86400 * 200) ,
grace_period_remaining: 0,
free_instances: 0
}, {
desc: 'should notify when license is invalid',
valid_key: false
},{
desc: 'should notify when license is empty',
}];
describe('Unit:CheckLicense', function() {
beforeEach(module('Tower'));
/*beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
}));*/
it('should contain CheckLicense service', inject(function(CheckLicense) {
expect(CheckLicense).not.toBe(null);
}));
it('should have a getRemainingDays method', inject(function(CheckLicense) {
expect(CheckLicense.getRemainingDays).not.toBe(null);
}));
it('should have a getHTML method', inject(function(CheckLicense) {
expect(CheckLicense.getHTML).not.toBe(null);
}));
it('should have a getAdmin method', inject(function(CheckLicense) {
expect(CheckLicense.getAdmin).not.toBe(null);
}));
it('should have a shouldNotify method', inject(function(CheckLicense) {
expect(CheckLicense.shouldNotify).not.toBe(null);
}));
it('should not notify when license valid, time remaining > 15 days and host count > 0', inject(function(CheckLicense) {
expect(CheckLicense.shouldNotify({
valid_key: true,
time_remaining: (86400 * 20),
grace_period_remaining: 0,
free_instances: 10 })).toBe(false);
}));
should_notify.forEach(function(lic) {
it(lic.desc, inject(function(CheckLicense) {
expect(CheckLicense.shouldNotify(lic)).toBe(true);
}));
});
licenses.forEach(function(lic) {
it(lic.desc, inject(function(CheckLicense) {
var r;
if (lic.expects) {
r = new RegExp(lic.expects);
expect(CheckLicense.getHTML(lic).body).toMatch(r);
} else {
r = new RegExp(lic.notExpects);
expect(CheckLicense.getHTML(lic).body).not.toMatch(r);
}
}));
});
it('should recognize empty license as invalid', inject(function(CheckLicense) {
expect(CheckLicense.getHTML({}).title).toMatch(/license required/i);
}));
it('should show license update form to admin users when license is invalid', inject(function(CheckLicense, $rootScope) {
$rootScope.current_user = {};
$rootScope.current_user.is_superuser = true;
expect(CheckLicense.getHTML({}).body).toMatch(/license\_license\_json/);
}));
it('should not show license update form to non-admin users when license is invalid', inject(function(CheckLicense, $rootScope) {
$rootScope.current_user = {};
$rootScope.current_user.is_superuser = false;
expect(CheckLicense.getHTML({}).body).not.toMatch(/license\_license\_json/);
}));
});

View File

@ -0,0 +1,88 @@
describe('Job Status Graph Directive', function() {
var element, scope, httpBackend;
var resizeHandler = sinon.spy();
beforeEach(module('Tower'));
beforeEach(module(function($provide) {
$provide.value('LoadBasePaths', angular.noop);
$provide.value('adjustGraphSize', resizeHandler);
}));
beforeEach(inject(function($rootScope, $compile, $httpBackend) {
httpBackend = $httpBackend;
$httpBackend.expectGET('/static/js/local_config.js').respond({
});
$httpBackend.whenGET('/static/partials/job_status_graph.html')
.respond("<div class='m'></div><div class='n'></div><div class='job-status-graph'><svg></svg></div>");
scope = $rootScope.$new();
element = '<job-status-graph class="job-status-graph" data="data" job-type="all" period="month"></job-status-graph>';
// Takes jobs grouped by result (successful or failure
// Then looks at each array of arrays, where index 0 is the timestamp & index 1 is the count of jobs with that status
scope.data =
{ jobs:
{ successful: [[1, 0], [2, 0], [3,0], [4,0], [5,0]],
failed: [[1,0],[2,0],[3,0],[4,0],[5,0]]
}
};
element = $compile(element)(scope);
scope.$digest();
$httpBackend.flush();
}));
afterEach(function() {
element.trigger('$destroy');
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
function filterDataSeries(key, data) {
return data.map(function(datum) {
return datum.values;
})[key];
}
it('uses successes & failures from scope', function() {
var chartContainer = d3.select(element.find('svg')[0]);
var lineData = chartContainer.datum();
var successfulSeries = filterDataSeries(0, lineData);
var failedSeries = filterDataSeries(1, lineData);
expect(successfulSeries).to.eql(
[ {x: 1, y: 0, series: 0},
{x: 2, y: 0, series: 0},
{x: 3, y: 0, series: 0},
{x: 4, y: 0, series: 0},
{x: 5, y: 0, series: 0}]);
expect(failedSeries).to.eql(
[ {x: 1, y: 0, series: 1},
{x: 2, y: 0, series: 1},
{x: 3, y: 0, series: 1},
{x: 4, y: 0, series: 1},
{x: 5, y: 0, series: 1}]);
});
it('cleans up external bindings', function() {
element.trigger('$destroy');
resizeHandler.reset();
inject(['$window', function($window) {
angular.element($window).trigger('resize');
}]);
expect(resizeHandler).not.to.have.been.called;
});
});

View File

@ -0,0 +1,131 @@
describe('Host Count Graph Data Service', function() {
var q;
var hostCountGraphData, httpBackend, rootScope, timeout;
var processErrors = sinon.spy();
var getBasePath = function(path) {
return '/' + path + '/';
}
function flushPromises() {
window.setTimeout(function() {
inject(function($rootScope) {
$rootScope.$apply();
});
});
}
function assertUrlDeferred(url, obj) {
if (angular.isUndefined(obj[url]) ||
angular.isUndefined(obj[url].then) &&
angular.isUndefined(obj[url].promise.then)) {
var urls = [];
for (key in obj) {
if (/\//.test(key)) {
urls.push(key);
}
}
var registered = urls.map(function(url) {
return "\t\"" + url + "\"";
}).join("\n");
throw "Could not find a thenable registered for url \"" + url + "\". Registered URLs include:\n\n" + registered + "\n\nPerhaps you typo'd the URL?\n"
}
}
var restStub = {
setUrl: function(url) {
restStub[url] = q.defer();
restStub.currentUrl = url;
},
reset: function() {
delete restStub.deferred;
},
get: function() {
// allow a single deferred on restStub in case we don't need URL
restStub.deferred = restStub[restStub.currentUrl];
return restStub.deferred.promise;
},
succeedAt: function(url, value) {
assertUrlDeferred(url, restStub);
restStub[url].resolve(value);
},
succeed: function(value) {
restStub.deferred.resolve(value);
},
failAt: function(url, value) {
assertUrlDeferred(url, restStub);
restStub[url].reject(value);
},
fail: function(value) {
restStub.deferred.reject(value);
}
};
beforeEach(module("Tower"));
beforeEach(module(function($provide) {
$provide.value("$cookieStore", { get: angular.noop });
$provide.value('Rest', restStub);
$provide.value('GetBasePath', getBasePath);
}));
afterEach(function() {
restStub.reset();
});
beforeEach(inject(function(_hostCountGraphData_, $httpBackend, $q, $rootScope, $timeout) {
hostCountGraphData = _hostCountGraphData_;
httpBackend = $httpBackend;
rootScope = $rootScope;
timeout = $timeout;
$httpBackend.expectGET('/static/js/local_config.js').respond({
});
q = $q;
}));
it('returns a promise to be fulfilled when data comes in', function() {
var license = "license";
var hostData = "hosts";
var result = hostCountGraphData.get();
restStub.succeedAt('/config/', { data: {
license_info: {
instance_count: license
}
}
});
restStub.succeedAt('/dashboard/graphs/inventory/', { data: hostData });
flushPromises();
return expect(result).to.eventually.eql({ license: license, hosts: hostData });;
});
it('processes errors through error handler', function() {
var expected = { data: "blah", status: "bad" };
var actual = hostCountGraphData.get();
restStub.failAt('/config/', expected);
flushPromises();
return actual.catch(function() {
expect(processErrors).to
.have.been.calledWith(null, expected.data, expected.status);
});
});
});

View File

@ -0,0 +1,124 @@
describe('Job Status Graph Data Service', function() {
var q;
var jobStatusGraphData, httpBackend, rootScope, timeout;
var jobStatusChange = {
$on: sinon.spy(),
};
var processErrors = sinon.spy();
var getBasePath = function(path) {
return '/' + path + '/';
}
function flushPromises() {
window.setTimeout(function() {
inject(function($rootScope) {
$rootScope.$apply();
});
});
}
var restStub = {
setUrl: angular.noop,
reset: function() {
delete restStub.deferred;
},
get: function() {
if (angular.isUndefined(restStub.deferred)) {
restStub.deferred = q.defer();
}
return restStub.deferred.promise;
},
succeed: function(value) {
restStub.deferred.resolve(value);
},
fail: function(value) {
restStub.deferred.reject(value);
}
};
beforeEach(module("Tower"));
beforeEach(module(function($provide) {
$provide.value("$cookieStore", { get: angular.noop });
$provide.value('ProcessErrors', processErrors);
$provide.value('Rest', restStub);
$provide.value('GetBasePath', getBasePath);
}));
afterEach(function() {
restStub.reset();
});
beforeEach(inject(function(_jobStatusGraphData_, $httpBackend, $q, $rootScope, $timeout) {
jobStatusGraphData = _jobStatusGraphData_;
httpBackend = $httpBackend;
rootScope = $rootScope;
timeout = $timeout;
$httpBackend.expectGET('/static/js/local_config.js').respond({
});
q = $q;
}));
it('returns a promise to be fulfilled when data comes in', function() {
var firstResult = "result";
var result = jobStatusGraphData.get('', '');
restStub.succeed({ data: firstResult });
flushPromises();
return expect(result).to.eventually.equal(firstResult);;
});
it('processes errors through error handler', function() {
var expected = { data: "blah", status: "bad" };
var actual = jobStatusGraphData.get().catch(function() {
return processErrors;
});
restStub.fail(expected);
flushPromises();
return actual.catch(function() {
expect(processErrors).to
.have.been.calledWith(null, expected.data, expected.status);
});
});
it('broadcasts event when data is received', function() {
var expected = "value";
var result = q.defer();
jobStatusGraphData.setupWatcher();
inject(function($rootScope) {
$rootScope.$on('DataReceived:JobStatusGraph', function(e, data) {
result.resolve(data);
});
$rootScope.$emit('JobStatusChange');
restStub.succeed({ data: expected });
flushPromises();
});
return expect(result.promise).to.eventually.equal(expected);
});
it('requests data with given period and jobType', function() {
restStub.setUrl = sinon.spy();
jobStatusGraphData.get('1', '2');
expect(restStub.setUrl).to.have.been.calledWith('/dashboard/graphs/jobs/?period=1&job_type=2');
});
});