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); + }); + }); });