From c094c27eff68557b121a4db944f68bb4f41bd43a Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Mon, 27 Apr 2015 02:40:47 -0700 Subject: [PATCH] Add results and test report page This is the groundwork for the community results listing page and the test run report page. The report page is fairly basic, primarily showing how results stack up against defcore capabilities. Design is subject to change, but this gets the ball rolling. A config.json.sample was also added. The UI now expects a config.json in the root which will contain the Refstack API URL. Change-Id: Id7a376d0bccda5cbb5daf05e52a2c174ad40b497 --- refstack-ui/.gitignore | 1 + refstack-ui/README.rst | 4 + refstack-ui/app/app.js | 35 +++- refstack-ui/app/assets/css/style.css | 12 +- .../components/capabilities/capabilities.html | 3 +- .../results-report/resultsReport.html | 179 ++++++++++++++++++ .../results-report/resultsReportController.js | 91 +++++++++ .../app/components/results/results.html | 83 +++++++- .../components/results/resultsController.js | 51 +++++ refstack-ui/app/config.json.sample | 1 + refstack-ui/app/index.html | 7 +- refstack-ui/bower.json | 1 + refstack-ui/tests/karma.conf.js | 2 + refstack-ui/tests/unit/ControllerSpec.js | 117 ++++++++++++ 14 files changed, 576 insertions(+), 11 deletions(-) create mode 100644 refstack-ui/app/components/results-report/resultsReport.html create mode 100644 refstack-ui/app/components/results-report/resultsReportController.js create mode 100644 refstack-ui/app/components/results/resultsController.js create mode 100644 refstack-ui/app/config.json.sample diff --git a/refstack-ui/.gitignore b/refstack-ui/.gitignore index e4109d51..1f0fa666 100644 --- a/refstack-ui/.gitignore +++ b/refstack-ui/.gitignore @@ -7,3 +7,4 @@ dist node_modules npm-debug.log app/assets/lib +app/config.json diff --git a/refstack-ui/README.rst b/refstack-ui/README.rst index 942d677a..31f56f09 100644 --- a/refstack-ui/README.rst +++ b/refstack-ui/README.rst @@ -7,6 +7,10 @@ User interface for interacting with the Refstack API. Setup ===== +Create a config.json file and specify your API endpoint inside this file: + +:code:`cp app/config.json.sample app/config.json` + You can start a development server by doing the following: Install NodeJS and NPM: diff --git a/refstack-ui/app/app.js b/refstack-ui/app/app.js index 5ad00355..4e2780e3 100644 --- a/refstack-ui/app/app.js +++ b/refstack-ui/app/app.js @@ -3,8 +3,11 @@ /* App Module */ var refstackApp = angular.module('refstackApp', [ - 'ui.router', 'ui.bootstrap']); + 'ui.router', 'ui.bootstrap', 'cgBusy']); +/* + * Handle application routing. + */ refstackApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise('/'); @@ -24,7 +27,33 @@ refstackApp.config(['$stateProvider', '$urlRouterProvider', }). state('results', { url: '/results', - templateUrl: '/components/results/results.html' + templateUrl: '/components/results/results.html', + controller: 'resultsController' + }). + state('resultsDetail', { + url: '/results/:testID', + templateUrl: '/components/results-report/resultsReport.html', + controller: 'resultsReportController' }) - }]); + } +]); +/* + * Load Config and start up the angular application. + */ +angular.element(document).ready(function () { + var $http = angular.injector(['ng']).get('$http'); + function startApp(config) { + // Add config options as constants. + for (var key in config) { + angular.module('refstackApp').constant(key, config[key]); + } + angular.bootstrap(document, ['refstackApp']); + } + + $http.get('config.json').success(function(data) { + startApp(data); + }).error(function(error) { + startApp({}); + }); +}); diff --git a/refstack-ui/app/assets/css/style.css b/refstack-ui/app/assets/css/style.css index 624a172c..75b59192 100644 --- a/refstack-ui/app/assets/css/style.css +++ b/refstack-ui/app/assets/css/style.css @@ -100,11 +100,6 @@ h1, h2, h3, h4, h5, h6 { content: '\00BB'; } -.flagged:before { - color: #E6A100; - content: '\2691'; -} - .program-about { font-size: .8em; } @@ -128,3 +123,10 @@ h1, h2, h3, h4, h5, h6 { width: 70%; height: 70%; } + +.result-filters { + padding-bottom: 10px; + border-top: 2px solid #C9C9C9; + border-bottom: 2px solid #C9C9C9; + margin-bottom: 15px; +} diff --git a/refstack-ui/app/components/capabilities/capabilities.html b/refstack-ui/app/components/capabilities/capabilities.html index 15a9eacf..8b8a831f 100644 --- a/refstack-ui/app/components/capabilities/capabilities.html +++ b/refstack-ui/app/components/capabilities/capabilities.html @@ -49,7 +49,8 @@ Tests ({{capability.tests.length}}) diff --git a/refstack-ui/app/components/results-report/resultsReport.html b/refstack-ui/app/components/results-report/resultsReport.html new file mode 100644 index 00000000..335704a8 --- /dev/null +++ b/refstack-ui/app/components/results-report/resultsReport.html @@ -0,0 +1,179 @@ +

Test Run Results

+ +
+
+ Test ID: {{testId}}
+ Upload Date: {{resultsData.created_at}} UTC
+ Duration: {{resultsData.duration_seconds}} seconds
+ Total Number of Passed Tests: {{resultsData.results.length}}
+ +
+ +

See how these results stack up against DefCore capabilities and OpenStack + target marketing programs. +

+ +
+
+ Capabilities Version: + +
+
+ Target Program: + +
+
+
+
+
+ Status: +
+
+ + {{caps.required.passedCount*100/caps.required.count | number:1}}% +
+
+

This cloud passes {{caps.required.passedCount*100/caps.required.count | number:1}}% ({{caps.required.passedCount}}/{{caps.required.count}}) + of the {{version}} capability tests required by the {{targetMappings[target]}} program.

+ +

Capability Overview

+ + + + + Required ({{caps.required.caps.length}} capabilities) + + +
    +
  1. + {{capability.id}} + + [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}] + + +
      +
    • + + + {{test}} +
    • +
    • + + + {{test}} +
    • +
    +
  2. +
+
+ + + + Advisory ({{caps.advisory.caps.length}} capabilities) + + +
    +
  1. + {{capability.id}} + + [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}] + + +
      +
    • + + + {{test}} +
    • +
    • + + + {{test}} +
    • +
    +
  2. +
+
+ + + + Deprecated ({{caps.deprecated.caps.length}} capabilities) + + +
    +
  1. + {{capability.id}} + + [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}] + + +
      +
    • + + + {{test}} +
    • +
    • + + + {{test}} +
    • +
    +
  2. +
+
+ + + + Removed ({{caps.removed.caps.length}} capabilities) + + +
    +
  1. + {{capability.id}} + + [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}] + + +
      +
    • + + + {{test}} +
    • +
    • + + + {{test}} +
    • +
    +
  2. +
+
+
+
+ + diff --git a/refstack-ui/app/components/results-report/resultsReportController.js b/refstack-ui/app/components/results-report/resultsReportController.js new file mode 100644 index 00000000..c6e86e50 --- /dev/null +++ b/refstack-ui/app/components/results-report/resultsReportController.js @@ -0,0 +1,91 @@ +'use strict'; + +/* Refstack Results Report Controller */ + +var refstackApp = angular.module('refstackApp'); + +refstackApp.controller('resultsReportController', ['$scope', '$http', '$stateParams', 'refstackApiUrl', + function($scope, $http, $stateParams, refstackApiUrl) { + $scope.testId = $stateParams.testID + $scope.version = '2015.03'; + $scope.hideTests = true; + $scope.target = 'platform'; + $scope.requiredOpen = true; + + $scope.targetMappings = { + 'platform': 'Openstack Powered Platform', + 'compute': 'OpenStack Powered Compute', + 'object': 'OpenStack Powered Object Storage' + } + + var content_url = refstackApiUrl +'/results/' + $scope.testId; + $scope.resultsRequest = $http.get(content_url).success(function(data) { + $scope.resultsData = data; + $scope.updateCapabilities(); + }).error(function(error) { + $scope.showError = true; + $scope.resultsData = null; + $scope.error = "Error retrieving results from server: " + JSON.stringify(error); + + }); + + $scope.updateCapabilities = function() { + $scope.showError = false; + var content_url = 'assets/capabilities/'.concat($scope.version, '.json'); + $http.get(content_url).success(function(data) { + $scope.capabilityData = data; + $scope.buildCapabilityObject($scope.capabilityData, $scope.resultsData.results); + }).error(function(error) { + $scope.showError = true; + $scope.capabilityData = null; + $scope.error = 'Error retrieving capabilities: ' + JSON.stringify(error); + }); + } + + $scope.buildCapabilityObject = function() { + var capabilities = $scope.capabilityData.capabilities; + var caps = {'required': {'caps': [], 'count': 0, 'passedCount': 0}, + 'advisory': {'caps': [], 'count': 0, 'passedCount': 0}, + 'deprecated': {'caps': [], 'count': 0, 'passedCount': 0}, + 'removed': {'caps': [], 'count': 0, 'passedCount': 0}}; + var components = $scope.capabilityData.components; + var cap_array = []; + // First determine which capabilities are relevant to the target. + if ($scope.target === 'platform') { + var platform_components = $scope.capabilityData.platform.required; + // For each component required for the platform program. + angular.forEach(platform_components, function(component) { + // Get each capability belonging to each status. + angular.forEach(components[component], function(capabilities) { + cap_array = cap_array.concat(capabilities); + }); + }); + } + else { + angular.forEach(components[$scope.target], function(capabilities) { + cap_array = cap_array.concat(capabilities); + }); + } + + angular.forEach(capabilities, function(value, key) { + if (cap_array.indexOf(key) > -1) { + var cap = { "id": key, + "passedTests": [], + "notPassedTests": []}; + caps[value.status].count += value.tests.length; + angular.forEach(value.tests, function(test_id) { + if ($scope.resultsData.results.indexOf(test_id) > -1) { + cap.passedTests.push(test_id); + } + else { + cap.notPassedTests.push(test_id); + } + }); + caps[value.status].passedCount += cap.passedTests.length; + caps[value.status].caps.push(cap); + } + }); + $scope.caps = caps; + } + } +]); diff --git a/refstack-ui/app/components/results/results.html b/refstack-ui/app/components/results/results.html index d2847d5f..13935e91 100644 --- a/refstack-ui/app/components/results/results.html +++ b/refstack-ui/app/components/results/results.html @@ -1 +1,82 @@ -

Community results list here.

+

Community Results

+

The most recently uploaded community test results are listed here. Currently, these results are anonymous.

+ +
+

Filters

+
+
+ +

+ + + + +

+
+
+ +

+ + + + +

+
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + +
Upload DateTest Run ID
{{result.created_at}}{{result.test_id}}
+ +
+ + +
+
+ + + diff --git a/refstack-ui/app/components/results/resultsController.js b/refstack-ui/app/components/results/resultsController.js new file mode 100644 index 00000000..ec4206f1 --- /dev/null +++ b/refstack-ui/app/components/results/resultsController.js @@ -0,0 +1,51 @@ +'use strict'; + +/* Refstack Results Controller */ + +var refstackApp = angular.module('refstackApp'); + +refstackApp.controller('resultsController', ['$scope', '$http', '$filter', 'refstackApiUrl', function($scope, $http, $filter, refstackApiUrl) { + $scope.currentPage = 1; + $scope.itemsPerPage = 20; + $scope.maxSize = 5; + $scope.startDate = ""; + $scope.endDate = ""; + $scope.update = function() { + $scope.showError = false; + var content_url = refstackApiUrl + '/results?page=' + $scope.currentPage; + var start = $filter('date')($scope.startDate, "yyyy-MM-dd"); + if (start) { + content_url = content_url + "&start_date=" + start + " 00:00:00"; + } + var end = $filter('date')($scope.endDate, "yyyy-MM-dd"); + if (end) { + content_url = content_url + "&end_date=" + end + " 23:59:59"; + } + + $scope.resultsRequest = $http.get(content_url).success(function(data) { + $scope.data = data; + $scope.totalItems = $scope.data.pagination.total_pages * $scope.itemsPerPage; + $scope.currentPage = $scope.data.pagination.current_page; + }).error(function(error) { + $scope.data = null; + $scope.totalItems = 0 + $scope.showError = true + $scope.error = "Error retrieving results listing from server: " + JSON.stringify(error); + }); + } + + $scope.update(); + + // This is called when a date filter calendar is opened. + $scope.open = function($event, openVar) { + $event.preventDefault(); + $event.stopPropagation(); + $scope[openVar] = true; + }; + + $scope.clearFilters = function() { + $scope.startDate = null; + $scope.endDate = null; + $scope.update(); + }; +}]); diff --git a/refstack-ui/app/config.json.sample b/refstack-ui/app/config.json.sample new file mode 100644 index 00000000..8cbb065e --- /dev/null +++ b/refstack-ui/app/config.json.sample @@ -0,0 +1 @@ +{"refstackApiUrl": "http://api.refstack.net/v1"} diff --git a/refstack-ui/app/index.html b/refstack-ui/app/index.html index bf4c81fd..314a3d43 100644 --- a/refstack-ui/app/index.html +++ b/refstack-ui/app/index.html @@ -14,7 +14,7 @@ License for the specific language governing permissions and limitations under the License. --> - + @@ -24,17 +24,22 @@ + + + + + diff --git a/refstack-ui/bower.json b/refstack-ui/bower.json index 0959df44..951db02f 100644 --- a/refstack-ui/bower.json +++ b/refstack-ui/bower.json @@ -7,6 +7,7 @@ "angular-ui-router": "0.2.13", "angular-resource": "1.3.15", "angular-bootstrap": "0.12.1", + "angular-busy": "4.1.3", "bootstrap": "3.3.2" }, "devDependencies": { diff --git a/refstack-ui/tests/karma.conf.js b/refstack-ui/tests/karma.conf.js index 2f6cb08b..f536f3aa 100644 --- a/refstack-ui/tests/karma.conf.js +++ b/refstack-ui/tests/karma.conf.js @@ -9,6 +9,8 @@ module.exports = function(config){ 'app/assets/lib/angular-ui-router/release/angular-ui-router.js', 'app/assets/lib/angular-bootstrap/ui-bootstrap.min.js', 'app/assets/lib/angular-mocks/angular-mocks.js', + 'app/assets/lib/angular-bootstrap/ui-bootstrap-tpls.min.js', + 'app/assets/lib/angular-busy/dist/angular-busy.min.js', // JS files. 'app/app.js', diff --git a/refstack-ui/tests/unit/ControllerSpec.js b/refstack-ui/tests/unit/ControllerSpec.js index 2c2bd673..b24f96b9 100644 --- a/refstack-ui/tests/unit/ControllerSpec.js +++ b/refstack-ui/tests/unit/ControllerSpec.js @@ -91,4 +91,121 @@ describe('Refstack controllers', function() { expect(scope.filterProgram({'id': 'cap_id_5'})).toBe(false); }); }); + + describe('resultsController', function() { + var scope, ctrl, $httpBackend, refstackApiUrl; + var fakeResponse = {'pagination': {'current_page': 1, 'total_pages': 2}, + 'results': [{'created_at': '2015-03-09 01:23:45', + 'test_id': 'some-id', + 'cpid': 'some-cpid'}]}; + + beforeEach(function() { + module('refstackApp'); + module(function($provide) { + $provide.constant('refstackApiUrl', 'http://foo.bar/v1'); + }); + }); + + beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { + $httpBackend = _$httpBackend_; + scope = $rootScope.$new(); + ctrl = $controller('resultsController', {$scope: scope}); + })); + + it('should fetch the first page of results with proper URL args', function() { + // Initial results should be page 1 of all results. + $httpBackend.expectGET('http://foo.bar/v1/results?page=1').respond(fakeResponse); + $httpBackend.flush(); + expect(scope.data).toEqual(fakeResponse); + expect(scope.currentPage).toBe(1); + + // Simulate the user adding date filters. + scope.startDate = new Date('2015-03-10T11:51:00'); + scope.endDate = new Date('2015-04-10T11:51:00'); + scope.update(); + $httpBackend.expectGET('http://foo.bar/v1/results?page=1&start_date=2015-03-10 00:00:00&end_date=2015-04-10 23:59:59').respond(fakeResponse); + $httpBackend.flush(); + expect(scope.data).toEqual(fakeResponse); + expect(scope.currentPage).toBe(1); + }); + + it('should set an error when results cannot be retrieved', function() { + $httpBackend.expectGET('http://foo.bar/v1/results?page=1').respond(404, {'detail': 'Not Found'}); + $httpBackend.flush(); + expect(scope.data).toBe(null); + expect(scope.error).toEqual('Error retrieving results listing from server: {"detail":"Not Found"}'); + expect(scope.totalItems).toBe(0); + expect(scope.showError).toBe(true); + }); + + it('should have an function to clear filters and update the view', function() { + $httpBackend.expectGET('http://foo.bar/v1/results?page=1').respond(fakeResponse); + scope.startDate = "some date"; + scope.endDate = "some other date"; + scope.clearFilters(); + expect(scope.startDate).toBe(null); + expect(scope.endDate).toBe(null); + $httpBackend.expectGET('http://foo.bar/v1/results?page=1').respond(fakeResponse); + $httpBackend.flush(); + expect(scope.data).toEqual(fakeResponse); + }); + }); + + describe('resultsReportController', function() { + var scope, ctrl, $httpBackend, refstackApiUrl, stateparams; + var fakeResultResponse = {'results': ['test_id_1']} + var fakeCapabilityResponse = {'platform': {'required': ['compute']}, + 'components': { + 'compute': { + 'required': ['cap_id_1'], + 'advisory': [], + 'deprecated': [], + 'removed': [] + } + }, + 'capabilities': { + 'cap_id_1': { + 'status': 'required', + 'flagged': [], + 'tests': ['test_id_1', 'test_id_2'] + } + } + }; + + beforeEach(function() { + module('refstackApp'); + module(function($provide) { + $provide.constant('refstackApiUrl', 'http://foo.bar/v1'); + }); + }); + + beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { + $httpBackend = _$httpBackend_; + stateparams = {testID: 1234}; + scope = $rootScope.$new(); + ctrl = $controller('resultsReportController', {$scope: scope, $stateParams: stateparams}); + })); + + it('should get the results for a specific test ID and also the relevant capabilities', function() { + $httpBackend.expectGET('http://foo.bar/v1/results/1234').respond(fakeResultResponse); + $httpBackend.expectGET('assets/capabilities/2015.03.json').respond(fakeCapabilityResponse); + $httpBackend.flush(); + expect(scope.resultsData).toEqual(fakeResultResponse); + expect(scope.capabilityData).toEqual(fakeCapabilityResponse); + }); + + it('should be able to sort the results into a capability object', function() { + scope.resultsData = fakeResultResponse; + scope.capabilityData = fakeCapabilityResponse; + scope.buildCapabilityObject(); + var expectedCapsObject = {'required': {'caps': [{'id': 'cap_id_1', + 'passedTests': ['test_id_1'], + 'notPassedTests': ['test_id_2']}], + 'count': 2, 'passedCount': 1}, + 'advisory': {'caps': [], 'count': 0, 'passedCount': 0}, + 'deprecated': {'caps': [], 'count': 0, 'passedCount': 0}, + 'removed': {'caps': [], 'count': 0, 'passedCount': 0}}; + expect(scope.caps).toEqual(expectedCapsObject); + }); + }); });