diff --git a/refstack-ui/.bowerrc b/refstack-ui/.bowerrc
new file mode 100644
index 00000000..5d2a4415
--- /dev/null
+++ b/refstack-ui/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "app/assets/lib"
+}
diff --git a/refstack-ui/.gitignore b/refstack-ui/.gitignore
new file mode 100644
index 00000000..e4109d51
--- /dev/null
+++ b/refstack-ui/.gitignore
@@ -0,0 +1,9 @@
+.tmp
+AUTHORS
+ChangeLog
+build/
+cover/
+dist
+node_modules
+npm-debug.log
+app/assets/lib
diff --git a/refstack-ui/README.rst b/refstack-ui/README.rst
new file mode 100644
index 00000000..942d677a
--- /dev/null
+++ b/refstack-ui/README.rst
@@ -0,0 +1,36 @@
+=======================
+Refstack User Interface
+=======================
+
+User interface for interacting with the Refstack API.
+
+Setup
+=====
+
+You can start a development server by doing the following:
+
+Install NodeJS and NPM:
+
+:code:`curl -sL https://deb.nodesource.com/setup | sudo bash -`
+
+:code:`sudo apt-get install nodejs`
+
+From the Refstack project root directory, move into the UI folder:
+
+:code:`cd refstack-ui`
+
+Install dependencies and start the server:
+
+:code:`npm start`
+
+Doing this will automatically perform :code:`npm start` and :code:`bower install`
+to get all dependencies.
+
+By default, as noted in package.json, the server will use 0.0.0.0:8080.
+
+Test
+====
+
+To run unit tests, simply perform the following:
+
+:code:`npm test`
diff --git a/refstack-ui/app/app.js b/refstack-ui/app/app.js
new file mode 100644
index 00000000..5ad00355
--- /dev/null
+++ b/refstack-ui/app/app.js
@@ -0,0 +1,30 @@
+'use strict';
+
+/* App Module */
+
+var refstackApp = angular.module('refstackApp', [
+ 'ui.router', 'ui.bootstrap']);
+
+refstackApp.config(['$stateProvider', '$urlRouterProvider',
+ function($stateProvider, $urlRouterProvider) {
+ $urlRouterProvider.otherwise('/');
+ $stateProvider.
+ state('home', {
+ url: '/',
+ templateUrl: '/components/home/home.html'
+ }).
+ state('about', {
+ url: '/about',
+ templateUrl: '/components/about/about.html'
+ }).
+ state('capabilities', {
+ url: '/capabilities',
+ templateUrl: '/components/capabilities/capabilities.html',
+ controller: 'capabilitiesController'
+ }).
+ state('results', {
+ url: '/results',
+ templateUrl: '/components/results/results.html'
+ })
+ }]);
+
diff --git a/refstack-ui/app/assets/capabilities/2015.03.json b/refstack-ui/app/assets/capabilities/2015.03.json
new file mode 120000
index 00000000..ac386c6d
--- /dev/null
+++ b/refstack-ui/app/assets/capabilities/2015.03.json
@@ -0,0 +1 @@
+../../../../defcore/2015.03.json
\ No newline at end of file
diff --git a/refstack-ui/app/assets/css/style.css b/refstack-ui/app/assets/css/style.css
new file mode 100644
index 00000000..624a172c
--- /dev/null
+++ b/refstack-ui/app/assets/css/style.css
@@ -0,0 +1,130 @@
+body {
+ background: white;
+ color: black;
+ font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif;
+}
+
+a {
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+.heading {
+ font-size: 3em;
+ font-weight: bold;
+ margin-bottom: 10px;
+ margin-top: 10px;
+}
+
+.heading img {
+ height: 50px;
+ vertical-align: text-bottom;
+}
+
+form {
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+fieldset {
+ border: 0;
+}
+
+input.error {
+ background: #FAFF78;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif;
+}
+
+.footer {
+ background: none repeat scroll 0% 0% #333;
+}
+
+.required {
+ color: #1D6503;
+}
+
+.advisory {
+ color: #9F8501;
+}
+
+.deprecated {
+ color: #B03838;
+}
+
+.removed {
+ color: #801601;
+}
+
+.checkbox {
+ word-spacing: 20px;
+ background: #F8F8F8;
+ padding: 10px;
+}
+
+.capabilities {
+ color: #4B4B4B;
+}
+
+.capabilities a, .criteria a {
+ cursor: pointer;
+}
+
+.capabilities .capability-list-item {
+ border-bottom: 2px solid #AFAFAF;
+ padding-bottom: .6em;
+}
+
+.capabilities .capability-name {
+ font-size: 1.3em;
+ font-weight: bold;
+ color: black;
+}
+
+#criteria {
+ color: #4B4B4B;
+}
+
+.criterion-name {
+ font-size: 1.1em;
+ font-weight: bold;
+}
+
+.list-inline li:before {
+ content: '\00BB';
+}
+
+.flagged:before {
+ color: #E6A100;
+ content: '\2691';
+}
+
+.program-about {
+ font-size: .8em;
+}
+
+.jumbotron .left {
+ width: 70%;
+}
+
+.container .jumbotron {
+ background: #F6F6F6;
+ border-top: 2px solid #C9C9C9;
+ border-bottom: 2px solid #C9C9C9;
+ border-radius: 0;
+}
+
+.jumbotron .right {
+ width: 30%;
+}
+
+.jumbotron img {
+ width: 70%;
+ height: 70%;
+}
diff --git a/refstack-ui/app/assets/img/openstack-logo.png b/refstack-ui/app/assets/img/openstack-logo.png
new file mode 100644
index 00000000..e425be6c
Binary files /dev/null and b/refstack-ui/app/assets/img/openstack-logo.png differ
diff --git a/refstack-ui/app/assets/img/refstack-logo.png b/refstack-ui/app/assets/img/refstack-logo.png
new file mode 100755
index 00000000..fc45f3ee
Binary files /dev/null and b/refstack-ui/app/assets/img/refstack-logo.png differ
diff --git a/refstack-ui/app/assets/js/refstack.js b/refstack-ui/app/assets/js/refstack.js
new file mode 100644
index 00000000..2ccf6b1f
--- /dev/null
+++ b/refstack-ui/app/assets/js/refstack.js
@@ -0,0 +1,3 @@
+'use strict';
+
+/* Miscellaneous Refstack JavaScript */
diff --git a/refstack-ui/app/components/about/about.html b/refstack-ui/app/components/about/about.html
new file mode 100644
index 00000000..82d72825
--- /dev/null
+++ b/refstack-ui/app/components/about/about.html
@@ -0,0 +1,93 @@
+
+ Overview
+
+Refstack intends on being THE source of tools for interoperability testing
+ of OpenStack clouds.
+Refstack provides users in the OpenStack community with a Tempest wrapper,
+ refstack-client, that helps to verify interoperability of their cloud
+ with other OpenStack clouds. It does so by validating any cloud
+ implementation against the OpenStack Tempest API tests.
+
+ Refstack and DefCore - The prototypical use case for Refstack provides
+ the DefCore Committee the tools for vendors and other users to run API
+ tests against their clouds to provide the DefCore committee with a reliable
+ overview of what APIs and capabilities are being used in the marketplace.
+ This will help to guide the DefCore-defined capabilities and help ensure
+ interoperability across the entire OpenStack ecosystem. It can also
+ be used to validate clouds against existing DefCore capability lists,
+ giving you assurance that your cloud faithfully implements OpenStack
+ standards.
+
+
+ Value Add for Vendors - Vendors can use Refstack to demonstrate that
+ their distros, and/or their customers' installed clouds remain with OpenStack
+ after their software has been incorporated into the distro or cloud.
+
+
+ Refstack consists of two parts:
+
+
+
+
+
+ refstack-api
+
+
+ Our API isn't just for us to collect data from private and public cloud
+ vendors. It can be used by vendors in house to compare interoperability
+ data over time.
+
+
+
+
+
+
+
+ refstack-client
+
+
+ refstack-client contains the tools you will need to run the DefCore tests.
+
+
+
+
+
+
+ Get involved!
+
+
diff --git a/refstack-ui/app/components/capabilities/capabilities.html b/refstack-ui/app/components/capabilities/capabilities.html
new file mode 100644
index 00000000..15a9eacf
--- /dev/null
+++ b/refstack-ui/app/components/capabilities/capabilities.html
@@ -0,0 +1,71 @@
+DefCore Capabilities
+Version:
+
+ 2015.03
+
+
+Target Program:
+
+ OpenStack Powered Platform
+ OpenStack Powered Compute
+ OpenStack Powered Object Storage
+
+About
+
+
+Capability Status:
+
+
+
+ Required
+
+
+
+ Advisory
+
+
+
+ Deprecated
+
+
+
+ Removed
+
+
+Tests marked with are tests flagged by DefCore.
+
+
+
+ {{capability.name}}
+ {{capability.description}}
+ Status: {{capability.status}}
+ Achievements ({{capability.achievements.length}})
+
+
+ {{achievement}}
+
+
+
+ Tests ({{capability.tests.length}})
+
+
+
+
+
+
+
+
+
+
+ {{criterion.name}}
+ {{criterion.Description}}
+ Weight: {{criterion.weight}}
+
+
+
+
+
diff --git a/refstack-ui/app/components/capabilities/capabilitiesController.js b/refstack-ui/app/components/capabilities/capabilitiesController.js
new file mode 100644
index 00000000..efc1b5d0
--- /dev/null
+++ b/refstack-ui/app/components/capabilities/capabilitiesController.js
@@ -0,0 +1,63 @@
+'use strict';
+
+/* Refstack Capabilities Controller */
+
+var refstackApp = angular.module('refstackApp');
+
+refstackApp.controller('capabilitiesController', ['$scope', '$http', function($scope, $http) {
+ $scope.version = '2015.03';
+ $scope.hideAchievements = true;
+ $scope.hideTests = true;
+ $scope.target = 'platform';
+ $scope.status = {
+ required: 'required',
+ advisory: '',
+ deprecated: '',
+ removed: ''
+ };
+
+ $scope.update = function() {
+ // Rate-limiting is an issue with this URL. Using a local copy for now.
+ // var content_url = 'https://api.github.com/repos/openstack/defcore/contents/'.concat($scope.version, '.json');
+ var content_url = 'assets/capabilities/'.concat($scope.version, '.json');
+ $http.get(content_url).success(function(data) {
+ //$scope.data = data;
+ //$scope.capabilities = JSON.parse(atob($scope.data.content.replace(/\s/g, '')));
+ $scope.capabilities = data;
+ }).error(function(error) {
+ console.log(error);
+ $scope.capabilities = 'Error retrieving capabilities.';
+ });
+ }
+ $scope.update()
+
+ $scope.filterProgram = function(capability){
+ var components = $scope.capabilities.components;
+ if ($scope.target === 'platform') {
+ var platform_components = $scope.capabilities.platform.required;
+ var cap_array = [];
+ // 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);
+ });
+ });
+ return (cap_array.indexOf(capability.id) > -1);
+ }
+ else {
+ var cap_array = [];
+ angular.forEach(components[$scope.target], function(capabilities) {
+ cap_array = cap_array.concat(capabilities);
+ });
+ return (cap_array.indexOf(capability.id) > -1);
+ }
+ };
+
+ $scope.filterStatus = function(capability){
+ return capability.status === $scope.status.required ||
+ capability.status === $scope.status.advisory ||
+ capability.status === $scope.status.deprecated ||
+ capability.status === $scope.status.removed;
+ };
+}]);
diff --git a/refstack-ui/app/components/home/home.html b/refstack-ui/app/components/home/home.html
new file mode 100644
index 00000000..5929645a
--- /dev/null
+++ b/refstack-ui/app/components/home/home.html
@@ -0,0 +1,31 @@
+
+
+
OpenStack Interoperability
+
Refstack is a source of tools for OpenStack interoperability testing.
+
+
+
+
+
+
+
+
+
+
What is Refstack?
+
+ Toolset for testing interoperability between OpenStack clouds.
+ Database backed website supporting collection and publication of
+ community test results for OpenStack.
+ User interface to display individual test run results.
+
+
+
+
+
OpenStack Marketing Programs
+
+ OpenStack Powered Platform
+ OpenStack Powered Compute
+ OpenStack Powered Object Storage
+
+
+
diff --git a/refstack-ui/app/components/results/results.html b/refstack-ui/app/components/results/results.html
new file mode 100644
index 00000000..d2847d5f
--- /dev/null
+++ b/refstack-ui/app/components/results/results.html
@@ -0,0 +1 @@
+Community results list here.
diff --git a/refstack-ui/app/favicon.ico b/refstack-ui/app/favicon.ico
new file mode 100644
index 00000000..156019aa
Binary files /dev/null and b/refstack-ui/app/favicon.ico differ
diff --git a/refstack-ui/app/index.html b/refstack-ui/app/index.html
new file mode 100644
index 00000000..bf4c81fd
--- /dev/null
+++ b/refstack-ui/app/index.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ Refstack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/refstack-ui/app/robots.txt b/refstack-ui/app/robots.txt
new file mode 100644
index 00000000..93c44208
--- /dev/null
+++ b/refstack-ui/app/robots.txt
@@ -0,0 +1,4 @@
+# robotstxt.org
+
+User-agent: *
+
diff --git a/refstack-ui/app/shared/filters.js b/refstack-ui/app/shared/filters.js
new file mode 100644
index 00000000..0b8b31f5
--- /dev/null
+++ b/refstack-ui/app/shared/filters.js
@@ -0,0 +1,18 @@
+'use strict';
+
+/* Refstack Filters */
+
+var refstackApp = angular.module('refstackApp');
+
+// Convert an object of objects to an array of objects to use with ng-repeat
+// filters.
+refstackApp.filter('arrayConverter', function() {
+ return function(objects) {
+ var array = [];
+ angular.forEach(objects, function(object, key) {
+ object['id'] = key;
+ array.push(object);
+ });
+ return array;
+ };
+});
diff --git a/refstack-ui/app/shared/header/header.html b/refstack-ui/app/shared/header/header.html
new file mode 100644
index 00000000..a42ddd6f
--- /dev/null
+++ b/refstack-ui/app/shared/header/header.html
@@ -0,0 +1,26 @@
+
+Refstack
+
+
+
+
+
diff --git a/refstack-ui/app/shared/header/headerController.js b/refstack-ui/app/shared/header/headerController.js
new file mode 100644
index 00000000..1690912b
--- /dev/null
+++ b/refstack-ui/app/shared/header/headerController.js
@@ -0,0 +1,18 @@
+'use strict';
+
+/* Refstack Header Controller */
+
+var refstackApp = angular.module('refstackApp')
+refstackApp.controller('headerController', ['$scope', '$location', function($scope, $location) {
+ $scope.navbarCollapsed = true;
+ $scope.isActive = function(viewLocation) {
+ var path = $location.path().substr(0, viewLocation.length);
+ if (path === viewLocation) {
+ // Make sure "/" only matches when viewLocation is "/".
+ if (!($location.path().substr(0).length > 1 && viewLocation.length === 1 )) {
+ return true;
+ }
+ }
+ return false;
+ };
+}]);
diff --git a/refstack-ui/bower.json b/refstack-ui/bower.json
new file mode 100644
index 00000000..0959df44
--- /dev/null
+++ b/refstack-ui/bower.json
@@ -0,0 +1,18 @@
+{
+ "name": "refstack-ui",
+ "version": "0.0.1",
+ "description": "Refstack user interface",
+ "dependencies": {
+ "angular": "1.3.15",
+ "angular-ui-router": "0.2.13",
+ "angular-resource": "1.3.15",
+ "angular-bootstrap": "0.12.1",
+ "bootstrap": "3.3.2"
+ },
+ "devDependencies": {
+ "angular-mocks": "1.3.15"
+ },
+ "resolutions": {
+ "angular": "1.3.15"
+ }
+}
diff --git a/refstack-ui/package.json b/refstack-ui/package.json
new file mode 100644
index 00000000..a6b37215
--- /dev/null
+++ b/refstack-ui/package.json
@@ -0,0 +1,28 @@
+{
+ "version": "0.0.1",
+ "private": true,
+ "name": "refstack-ui",
+ "description": "A user interface for Refstack",
+ "license": "Apache2",
+ "devDependencies": {
+ "karma": "^0.12.23",
+ "karma-chrome-launcher": "^0.1.5",
+ "karma-jasmine": "^0.2.2",
+ "karma-firefox-launcher": "^0.1.3",
+ "protractor": "~1.0.0",
+ "http-server": "^0.6.1",
+ "tmp": "0.0.23",
+ "bower": "1.3.12",
+ "shelljs": "^0.2.6"
+ },
+ "scripts": {
+ "postinstall": "bower install",
+
+ "prestart": "npm install",
+ "start": "http-server ./app -a 0.0.0.0 -p 8080",
+
+ "pretest": "npm install",
+ "test": "node node_modules/karma/bin/karma start tests/karma.conf.js",
+ "test-single-run": "node node_modules/karma/bin/karma start tests/karma.conf.js --single-run"
+ }
+}
diff --git a/refstack-ui/tests/karma.conf.js b/refstack-ui/tests/karma.conf.js
new file mode 100644
index 00000000..2f6cb08b
--- /dev/null
+++ b/refstack-ui/tests/karma.conf.js
@@ -0,0 +1,42 @@
+module.exports = function(config){
+ config.set({
+
+ basePath : '../',
+
+ files : [
+ // Angular libraries.
+ 'app/assets/lib/angular/angular.js',
+ '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',
+
+ // JS files.
+ 'app/app.js',
+ 'app/components/**/*.js',
+ 'app/shared/*.js',
+ 'app/shared/**/*.js',
+ 'app/assets/js/*.js',
+
+ // Test Specs.
+ 'tests/unit/*.js'
+ ],
+
+ autoWatch : true,
+
+ frameworks: ['jasmine'],
+
+ browsers : ['Firefox'],
+
+ plugins : [
+ 'karma-chrome-launcher',
+ 'karma-firefox-launcher',
+ 'karma-jasmine',
+ ],
+
+ junitReporter : {
+ outputFile: 'test_out/unit.xml',
+ suite: 'unit'
+ }
+
+ });
+};
diff --git a/refstack-ui/tests/unit/ControllerSpec.js b/refstack-ui/tests/unit/ControllerSpec.js
new file mode 100644
index 00000000..2c2bd673
--- /dev/null
+++ b/refstack-ui/tests/unit/ControllerSpec.js
@@ -0,0 +1,94 @@
+'use strict';
+
+/* Jasmine specs for Refstack controllers */
+describe('Refstack controllers', function() {
+
+ describe('headerController', function() {
+ var scope, ctrl, $location;
+ beforeEach(module('refstackApp'));
+
+ beforeEach(inject(function($rootScope, $controller, _$location_) {
+ scope = $rootScope.$new();
+ $location = _$location_;
+ ctrl = $controller('headerController', {$scope: scope});
+ }));
+
+ it('should set "navbarCollapsed" to true', function() {
+ expect(scope.navbarCollapsed).toBe(true);
+ });
+
+ it('should have a function to check if the URL path is active', function() {
+ $location.path('/');
+ expect($location.path()).toBe('/');
+ expect(scope.isActive('/')).toBe(true);
+ expect(scope.isActive('/about')).toBe(false);
+
+ $location.path('/results?cpid=123&foo=bar');
+ expect($location.path()).toBe('/results?cpid=123&foo=bar');
+ expect(scope.isActive('/results')).toBe(true);
+ });
+ });
+
+ describe('capabilitiesController', function() {
+ var scope, ctrl, $httpBackend;
+ beforeEach(module('refstackApp'));
+
+ beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
+ $httpBackend = _$httpBackend_;
+ scope = $rootScope.$new();
+ ctrl = $controller('capabilitiesController', {$scope: scope});
+ }));
+
+ it('should set default states', function() {
+ expect(scope.hideAchievements).toBe(true);
+ expect(scope.hideTests).toBe(true);
+ expect(scope.version).toBe('2015.03');
+ expect(scope.target).toBe('platform');
+ expect(scope.status).toEqual({required: 'required', advisory: '',
+ deprecated: '', removed: ''});
+
+ });
+
+ it('should fetch the selected capabilities version', function() {
+ $httpBackend.expectGET('assets/capabilities/2015.03.json').respond({'foo': 'bar'});
+ $httpBackend.flush();
+ expect(scope.capabilities).toEqual({'foo': 'bar'});
+ });
+
+ it('should have a function to check if a status filter is selected', function() {
+ expect(scope.filterStatus({'status': 'required'})).toBe(true);
+ expect(scope.filterStatus({'status': 'advisory'})).toBe(false);
+ expect(scope.filterStatus({'status': 'deprecated'})).toBe(false);
+ expect(scope.filterStatus({'status': 'removed'})).toBe(false);
+
+ scope.status = {
+ required: 'required',
+ advisory: 'advisory',
+ deprecated: 'deprecated',
+ removed: 'removed'
+ };
+
+ expect(scope.filterStatus({'status': 'required'})).toBe(true);
+ expect(scope.filterStatus({'status': 'advisory'})).toBe(true);
+ expect(scope.filterStatus({'status': 'deprecated'})).toBe(true);
+ expect(scope.filterStatus({'status': 'removed'})).toBe(true);
+ });
+
+ it('should have a function to check if a capability belongs to a program', function() {
+ scope.capabilities = {'platform': {'required': ['compute']},
+ 'components': {
+ 'compute': {
+ 'required': ['cap_id_1'],
+ 'advisory': ['cap_id_2'],
+ 'deprecated': ['cap_id_3'],
+ 'removed': ['cap_id_4']
+ }
+ }};
+ expect(scope.filterProgram({'id': 'cap_id_1'})).toBe(true);
+ expect(scope.filterProgram({'id': 'cap_id_2'})).toBe(true);
+ expect(scope.filterProgram({'id': 'cap_id_3'})).toBe(true);
+ expect(scope.filterProgram({'id': 'cap_id_4'})).toBe(true);
+ expect(scope.filterProgram({'id': 'cap_id_5'})).toBe(false);
+ });
+ });
+});
diff --git a/refstack-ui/tests/unit/FilterSpec.js b/refstack-ui/tests/unit/FilterSpec.js
new file mode 100644
index 00000000..b185acce
--- /dev/null
+++ b/refstack-ui/tests/unit/FilterSpec.js
@@ -0,0 +1,20 @@
+'use strict';
+
+/* Jasmine specs for Refstack filters */
+describe('Refstack filters', function() {
+
+ describe('Filter: arrayConverter', function() {
+ var $filter;
+ beforeEach(module('refstackApp'));
+ beforeEach(inject(function(_$filter_) {
+ $filter = _$filter_('arrayConverter');
+ }));
+
+ it('should convert dict to array of dict values', function () {
+ var object = { 'id1': {'key1': 'value1'}, 'id2': {'key2': 'value2'}};
+ var expected = [{'key1': 'value1', 'id': 'id1'},
+ {'key2': 'value2', 'id': 'id2'}];
+ expect($filter(object)).toEqual(expected);
+ });
+ });
+});