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:
+
+ 2015.03
+
+
+
+ Target Program:
+
+ OpenStack Powered Platform
+ OpenStack Powered Compute
+ OpenStack Powered Object Storage
+
+
+
+
+
+
+
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)
+
+
+
+
+ {{capability.id}}
+
+ [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}]
+
+
+
+
+
+
+ {{test}}
+
+
+
+
+ {{test}}
+
+
+
+
+
+
+
+
+ Advisory ({{caps.advisory.caps.length}} capabilities)
+
+
+
+
+ {{capability.id}}
+
+ [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}]
+
+
+
+
+
+
+ {{test}}
+
+
+
+
+ {{test}}
+
+
+
+
+
+
+
+
+ Deprecated ({{caps.deprecated.caps.length}} capabilities)
+
+
+
+
+ {{capability.id}}
+
+ [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}]
+
+
+
+
+
+
+ {{test}}
+
+
+
+
+ {{test}}
+
+
+
+
+
+
+
+
+ Removed ({{caps.removed.caps.length}} capabilities)
+
+
+
+
+ {{capability.id}}
+
+ [{{capability.passedTests.length}}/{{capability.passedTests.length + capability.notPassedTests.length}}]
+
+
+
+
+
+
+ {{test}}
+
+
+
+
+ {{test}}
+
+
+
+
+
+
+
+
+
+
+ Error:
+ {{error}}
+
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
+
+
+
Start Date
+
+
+
+
+
+
+
+
+
+
+
End Date
+
+
+
+
+
+
+
+
+
+
+ Filter
+ Clear
+
+
+
+
+
+
+
+
+
+ Upload Date
+ Test Run ID
+
+
+
+
+
+ {{result.created_at}}
+ {{result.test_id}}
+
+
+
+
+
+
+
+
+
+
+
+ Error:
+ {{error}}
+
+
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);
+ });
+ });
});