diff --git a/.gitignore b/.gitignore index 5994375544..0f07b6eaa4 100644 --- a/.gitignore +++ b/.gitignore @@ -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/** diff --git a/.jshintrc b/.jshintrc index 81365330d8..8562d25f2e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -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 } diff --git a/Makefile b/Makefile index 0bc3af431d..d1f2f03dd3 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index d551d6af11..a777d252c3 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -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', { diff --git a/awx/ui/static/js/config.js b/awx/ui/static/js/config.js index abed710242..3531acafc0 100644 --- a/awx/ui/static/js/config.js +++ b/awx/ui/static/js/config.js @@ -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. diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index 820ef907c9..8ecb0fcb81 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -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("
"); - } 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' -]; \ No newline at end of file +]; diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index 7719229cd9..2238c81698 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -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' -]; \ No newline at end of file +]; diff --git a/awx/ui/static/js/directives/auto-size-module.js b/awx/ui/static/js/directives/auto-size-module.js new file mode 100644 index 0000000000..2df92c53fd --- /dev/null +++ b/awx/ui/static/js/directives/auto-size-module.js @@ -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(); + }); + + }; + +}]); diff --git a/awx/ui/static/js/directives/dashboard-graphs.js b/awx/ui/static/js/directives/dashboard-graphs.js new file mode 100644 index 0000000000..ed86866e80 --- /dev/null +++ b/awx/ui/static/js/directives/dashboard-graphs.js @@ -0,0 +1 @@ +angular.module('DashboardGraphs', []); diff --git a/awx/ui/static/js/directives/host-count-graph.js b/awx/ui/static/js/directives/host-count-graph.js new file mode 100644 index 0000000000..bffb5f8c76 --- /dev/null +++ b/awx/ui/static/js/directives/host-count-graph.js @@ -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; + + } + } +}]); diff --git a/awx/ui/static/js/directives/host-status-graph.js b/awx/ui/static/js/directives/host-status-graph.js new file mode 100644 index 0000000000..11b0dbf227 --- /dev/null +++ b/awx/ui/static/js/directives/host-status-graph.js @@ -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 ''+x+''+ '

' + Math.floor(y.replace(',','')) + ' Hosts ' + '

'; + }) + .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 = $('
'); + notFoundContainer.css({ + 'text-align': 'center', + 'width': '100%', + 'padding-top': '2em' + }); + + notFoundContainer.text('No host data'); + + element.find('svg').replaceWith(notFoundContainer); + } + + } + } + }]); diff --git a/awx/ui/static/js/directives/job-status-graph.js b/awx/ui/static/js/directives/job-status-graph.js new file mode 100644 index 0000000000..3cf6696bf2 --- /dev/null +++ b/awx/ui/static/js/directives/job-status-graph.js @@ -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(""+this.text+"\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(""+this.text+"\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(); + } + + } + }]); diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 6c240530b6..0b34c48d2f 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -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) { diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index f71e9850fe..87f8aa38f4 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -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; }); diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index e8e89ed40e..90bf9e62ca 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -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 }); }; } -]); \ No newline at end of file +]); diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index e161167605..52d52f96b5 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -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({ diff --git a/awx/ui/static/js/helpers/Permissions.js b/awx/ui/static/js/helpers/Permissions.js index bc263549cb..de2a64156b 100644 --- a/awx/ui/static/js/helpers/Permissions.js +++ b/awx/ui/static/js/helpers/Permissions.js @@ -34,8 +34,8 @@ angular.module('PermissionsHelper', []) } else { scope.projectrequired = true; html = "
\n" + - "
Create
\n" + - "
Allow the user or team to create job templates. This implies that they have the Run and Check permissions.
\n" + + "
Create
\n" + + "
Allow the user or team to create job templates. This implies that they have the Run and Check permissions.
\n" + "
Run
\n" + "
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.
\n" + diff --git a/awx/ui/static/js/helpers/Survey.js b/awx/ui/static/js/helpers/Survey.js index 2ba260f1d9..889922ebcc 100644 --- a/awx/ui/static/js/helpers/Survey.js +++ b/awx/ui/static/js/helpers/Survey.js @@ -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'); }; diff --git a/awx/ui/static/js/services/adjust-graph-size.js b/awx/ui/static/js/services/adjust-graph-size.js new file mode 100644 index 0000000000..e447f94b38 --- /dev/null +++ b/awx/ui/static/js/services/adjust-graph-size.js @@ -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(); + }; +}); diff --git a/awx/ui/static/js/services/data-services.js b/awx/ui/static/js/services/data-services.js new file mode 100644 index 0000000000..fb6b4dc4fd --- /dev/null +++ b/awx/ui/static/js/services/data-services.js @@ -0,0 +1 @@ +angular.module('DataServices', []); diff --git a/awx/ui/static/js/services/host-count-graph-data.js b/awx/ui/static/js/services/host-count-graph-data.js new file mode 100644 index 0000000000..4bc1f817c3 --- /dev/null +++ b/awx/ui/static/js/services/host-count-graph-data.js @@ -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; + }); + } + }; +} diff --git a/awx/ui/static/js/services/job-status-graph-data.js b/awx/ui/static/js/services/job-status-graph-data.js new file mode 100644 index 0000000000..242368fc0f --- /dev/null +++ b/awx/ui/static/js/services/job-status-graph-data.js @@ -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); + + } + }; +} diff --git a/awx/ui/static/js/widgets/HostGraph.js b/awx/ui/static/js/widgets/HostGraph.js deleted file mode 100644 index 981268d788..0000000000 --- a/awx/ui/static/js/widgets/HostGraph.js +++ /dev/null @@ -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 = "
\n"; - html ="
\n"; - html += "
Host Count
\n"; - html += "
\n"; - html +="
\n"; - html += "
\n"; - - // html += "
\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; - - }, - - }); - //}); - }); - - - - }; - } - ]); \ No newline at end of file diff --git a/awx/ui/static/js/widgets/HostPieChart.js b/awx/ui/static/js/widgets/HostPieChart.js deleted file mode 100644 index 98bcf8ef21..0000000000 --- a/awx/ui/static/js/widgets/HostPieChart.js +++ /dev/null @@ -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 = "
\n"; - - html ="
\n"; - html += "
Host Status
\n"; - html += "
\n"; - - html +="
\n"; - html += "
\n"; - html += "
\n"; - - // html += "
\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 ''+x+''+ '

' + Math.floor(y.replace(',','')) + ' Hosts ' + '

'; - }) - .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 = 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'); - } - }; - } - ]); diff --git a/awx/ui/static/js/widgets/JobStatusGraph.js b/awx/ui/static/js/widgets/JobStatusGraph.js deleted file mode 100644 index 6477ca7b6f..0000000000 --- a/awx/ui/static/js/widgets/JobStatusGraph.js +++ /dev/null @@ -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 = "
\n"; - - html = "
\n"; - html += "
Job Status
\n"; // for All Jobs, Past Month - - html += "
\n"; - html += "
\n"; - html += "Job Type: \n"; - html += "All\n"; - html += " \n"; - - html += "\n"; - html += "
\n"; - - html += "
\n"; //end of filter div - - html += "
\n"; - html += "
\n"; - html += "Period: \n"; - html += "Past Month\n"; - html += " \n"; - - html += "\n"; - html += "
\n"; - html += "
\n"; //end of filter div - - html += "
\n"; // end of row - - html +="
\n"; - html += "
\n"; - html += "
\n"; - - // html += "
\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(""+this.text+"\n"); - - createGraph(); - }); - - //On click, update with new data - d3.selectAll(".m") - .on("click", function() { - job_type = this.getAttribute("id"); - $('#type-dropdown').replaceWith(""+this.text+"\n"); - - createGraph(); - }); - - scope.$emit('WidgetLoaded'); - return job_status_chart; - - }, - - - }); - - }); - - }; - } - ]); \ No newline at end of file diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index bc741ca16a..702191d940 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -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"; diff --git a/awx/ui/static/less/new-dashboard.less b/awx/ui/static/less/dashboard.less similarity index 86% rename from awx/ui/static/less/new-dashboard.less rename to awx/ui/static/less/dashboard.less index 2560cca8c1..6fcbe7289c 100644 --- a/awx/ui/static/less/new-dashboard.less +++ b/awx/ui/static/less/dashboard.less @@ -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; -} \ No newline at end of file +} diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index 7af00374ba..4d8765aa35 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -49,7 +49,6 @@ angular.module('Utilities', ['RestServices', 'Utilities']) } catch (e) { // ignore } - $(window).unbind('resize'); }; }]) @@ -859,4 +858,4 @@ angular.module('Utilities', ['RestServices', 'Utilities']) }; } -]); \ No newline at end of file +]); diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index ebda3089d7..51b79bf818 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -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; diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 7089ec804f..66f164a385 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -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 } }; } -]); \ No newline at end of file +]); diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index 5dafeec78e..aed5faa4cc 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -186,7 +186,6 @@ angular.module('ListGenerator', ['GeneratorHelpers']) // remove lingering popover
. 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}}\" />"; } - else // its assumed that options.input_type = checkbox + else { // its assumed that options.input_type = checkbox html += ""; + } } else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) { // Row level actions @@ -582,4 +582,4 @@ angular.module('ListGenerator', ['GeneratorHelpers']) return html; } }; - }]); \ No newline at end of file + }]); diff --git a/awx/ui/static/lib/ansible/prompt-dialog.js b/awx/ui/static/lib/ansible/prompt-dialog.js index a0c7f3a064..8bbe8692c6 100644 --- a/awx/ui/static/lib/ansible/prompt-dialog.js +++ b/awx/ui/static/lib/ansible/prompt-dialog.js @@ -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']) }; } - ]); \ No newline at end of file + ]); diff --git a/awx/ui/static/partials/home.html b/awx/ui/static/partials/home.html index 844d6b15fd..095222dff4 100644 --- a/awx/ui/static/partials/home.html +++ b/awx/ui/static/partials/home.html @@ -1,5 +1,5 @@ -
+
@@ -12,12 +12,24 @@
-
-
+
+
+ +
+
+
+
+ +
+
-
+
+
+ +
+
diff --git a/awx/ui/static/partials/host_count_graph.html b/awx/ui/static/partials/host_count_graph.html new file mode 100644 index 0000000000..3b154ab50a --- /dev/null +++ b/awx/ui/static/partials/host_count_graph.html @@ -0,0 +1,12 @@ +
+
+
+ Host Count +
+
+ +
+ +
+
+ diff --git a/awx/ui/static/partials/host_status_graph.html b/awx/ui/static/partials/host_status_graph.html new file mode 100644 index 0000000000..57193bf023 --- /dev/null +++ b/awx/ui/static/partials/host_status_graph.html @@ -0,0 +1,12 @@ +
+
+
+ Host Status +
+
+ +
+ +
+
+ diff --git a/awx/ui/static/partials/job_status_graph.html b/awx/ui/static/partials/job_status_graph.html new file mode 100644 index 0000000000..c57c242b30 --- /dev/null +++ b/awx/ui/static/partials/job_status_graph.html @@ -0,0 +1,39 @@ +
+ diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 59572635ff..d158fda7ee 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -81,6 +81,15 @@ + + + + + + + + + @@ -170,9 +179,6 @@ - - - diff --git a/awx/ui/tests/karma-unit.conf b/awx/ui/tests/karma-unit.conf index 05239f9cb6..eae72dd9e0 100644 --- a/awx/ui/tests/karma-unit.conf +++ b/awx/ui/tests/karma-unit.conf @@ -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 diff --git a/awx/ui/tests/karma-shared.conf b/awx/ui/tests/karma.conf.js similarity index 80% rename from awx/ui/tests/karma-shared.conf rename to awx/ui/tests/karma.conf.js index 36971fd92d..12a315cf88 100644 --- a/awx/ui/tests/karma-shared.conf +++ b/awx/ui/tests/karma.conf.js @@ -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 - }; + }); }; diff --git a/awx/ui/tests/unit/CheckLicense.js b/awx/ui/tests/unit/CheckLicense.js deleted file mode 100644 index c591e3fcaf..0000000000 --- a/awx/ui/tests/unit/CheckLicense.js +++ /dev/null @@ -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/); - })); -}); diff --git a/awx/ui/tests/unit/directives/job-status-graph-test.js b/awx/ui/tests/unit/directives/job-status-graph-test.js new file mode 100644 index 0000000000..18f5596916 --- /dev/null +++ b/awx/ui/tests/unit/directives/job-status-graph-test.js @@ -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("
"); + + scope = $rootScope.$new(); + + element = ''; + + // 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; + }); + +}); + diff --git a/awx/ui/tests/unit/services/host-count-graph-data-test.js b/awx/ui/tests/unit/services/host-count-graph-data-test.js new file mode 100644 index 0000000000..ed5b0699ec --- /dev/null +++ b/awx/ui/tests/unit/services/host-count-graph-data-test.js @@ -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); + }); + + }); + +}); + diff --git a/awx/ui/tests/unit/services/job-status-graph-data-test.js b/awx/ui/tests/unit/services/job-status-graph-data-test.js new file mode 100644 index 0000000000..fc0f4cd4bd --- /dev/null +++ b/awx/ui/tests/unit/services/job-status-graph-data-test.js @@ -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'); + }); + +});