summaryrefslogtreecommitdiffstats
path: root/utils/test/testapi/3rd_party/static/testapi-ui/components
diff options
context:
space:
mode:
authorSerenaFeng <feng.xiaowei@zte.com.cn>2017-05-12 01:49:57 +0800
committerSerenaFeng <feng.xiaowei@zte.com.cn>2017-05-12 10:11:57 +0800
commitf562c31e824f573d9a3254a1eacb4981b29290eb (patch)
treefd5526fc049fae9760da27b64318ad3fe5ce5767 /utils/test/testapi/3rd_party/static/testapi-ui/components
parenta16b903c9765049bd28102c812b8307090a97e16 (diff)
add web portal framework for TestAPI
Change-Id: I62cea8b59ffe6a6cde98051c130f4502c07d3557 Signed-off-by: SerenaFeng <feng.xiaowei@zte.com.cn>
Diffstat (limited to 'utils/test/testapi/3rd_party/static/testapi-ui/components')
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/about/about.html32
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/auth-failure/authFailureController.js33
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelines.html80
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelinesController.js322
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/guidelineDetails.html50
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/testListModal.html46
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/home/home.html23
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logout.html1
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logoutController.js44
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/profile/importPubKeyModal.html27
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profile.html37
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profileController.js219
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/profile/showPubKeyModal.html11
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html65
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html13
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html87
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html185
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js869
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results/results.html247
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js339
20 files changed, 2730 insertions, 0 deletions
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/about/about.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/about/about.html
new file mode 100644
index 000000000..65860a8cc
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/about/about.html
@@ -0,0 +1,32 @@
+<h1>TestAPI Documentation</h1>
+
+<p>TestAPI is a source of tools for test results collection of OPNFV clouds.</p>
+<p>To learn more about TestAPI, visit the links below.</p>
+
+<ol>
+ <li>
+ <a href="https://wiki.opnfv.org/pages/viewpage.action?pageId=2926452#testapi"
+ target="_blank"
+ </a>
+ <strong>About TestAPI</strong>
+ </li>
+ <li>
+ <a href="https://wiki.opnfv.org/pages/viewpage.action?pageId=6825128#how-to-declare-test-project-in-testapi"
+ target="_blank"
+ </a>
+ <strong>How do I declare project in TestAPI</strong>
+ </li>
+ <li>
+ <a href="https://#how-to-declare-test-case-in-testapi"
+ target="_blank"
+ </a>
+ <strong>How do I declare test case in TestAPI</strong>
+ </li>
+ <li>
+ <a href="https://wiki.opnfv.org/pages/viewpage.action?pageId=6825133#how-to-push-result-to-testapi"
+ target="_blank"
+ </a>
+ <strong>How do I push my results to TestAPI</strong>
+ </li>
+</ol>
+
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/auth-failure/authFailureController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/auth-failure/authFailureController.js
new file mode 100644
index 000000000..29d1d70fa
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/auth-failure/authFailureController.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('testapiApp')
+ .controller('AuthFailureController', AuthFailureController);
+
+ AuthFailureController.$inject = ['$location', '$state', 'raiseAlert'];
+ /**
+ * TestAPI Auth Failure Controller
+ * This controller handles messages from TestAPI API if user auth fails.
+ */
+ function AuthFailureController($location, $state, raiseAlert) {
+ var ctrl = this;
+ ctrl.message = $location.search().message;
+ raiseAlert('danger', 'Authentication Failure:', ctrl.message);
+ $state.go('home');
+ }
+})();
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelines.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelines.html
new file mode 100644
index 000000000..1dd39ff17
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelines.html
@@ -0,0 +1,80 @@
+<h3>OpenStack Powered&#8482; Guidelines</h3>
+
+<!-- Guideline Filters -->
+<div class="row">
+ <div class="col-md-3">
+ <strong>Version:</strong>
+ <!-- Slicing the version file name here gets rid of the '.json' file extension -->
+ <select ng-model="ctrl.version"
+ ng-change="ctrl.update()"
+ class="form-control"
+ ng-options="versionFile.slice(0,-5) for versionFile in ctrl.versionList">
+ </select>
+ </div>
+ <div class="col-md-4">
+ <strong>Target Program:</strong>
+ <span class="program-about"><a target="_blank" href="http://www.openstack.org/brand/interop/">About</a></span>
+ <select ng-model="ctrl.target" class="form-control" ng-change="ctrl.updateTargetCapabilities()">
+ <option value="platform">OpenStack Powered Platform</option>
+ <option value="compute">OpenStack Powered Compute</option>
+ <option value="object">OpenStack Powered Object Storage</option>
+ </select>
+ </div>
+</div>
+
+<br />
+<div ng-if="ctrl.guidelines">
+ <strong>Guideline Status:</strong>
+ {{ctrl.guidelines.status | capitalize}}
+</div>
+
+<div ng-show="ctrl.guidelines">
+ <strong>Corresponding OpenStack Releases:</strong>
+ <ul class="list-inline">
+ <li ng-repeat="release in ctrl.guidelines.releases">
+ {{release | capitalize}}
+ </li>
+ </ul>
+</div>
+
+<strong>Capability Status:</strong>
+<div class="checkbox">
+ <label>
+ <input type="checkbox" ng-model="ctrl.status.required">
+ <span class="required">Required</span>
+ </label>
+ <label>
+ <input type="checkbox" ng-model="ctrl.status.advisory">
+ <span class="advisory">Advisory</span>
+ </label>
+ <label>
+ <input type="checkbox" ng-model="ctrl.status.deprecated">
+ <span class="deprecated">Deprecated</span>
+ </label>
+ <label>
+ <input type="checkbox" ng-model="ctrl.status.removed">
+ <span class="removed">Removed</span>
+ </label>
+ <a class="test-list-dl pull-right"
+ title="Get a test list for capabilities matching selected statuses."
+ ng-click="ctrl.openTestListModal()">
+
+ Test List <span class="glyphicon glyphicon-file"></span>
+ </a>
+</div>
+<!-- End Capability Filters -->
+
+<p><small>Tests marked with <span class="glyphicon glyphicon-flag text-warning"></span> are tests flagged by Interop Working Group.</small></p>
+
+<!-- Loading animation divs -->
+<div cg-busy="{promise:ctrl.versionsRequest,message:'Loading versions'}"></div>
+<div cg-busy="{promise:ctrl.capsRequest,message:'Loading capabilities'}"></div>
+
+<!-- Get the version-specific template -->
+<div ng-include src="ctrl.detailsTemplate"></div>
+
+<div ng-show="ctrl.showError" class="alert alert-danger" role="alert">
+ <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+ <span class="sr-only">Error:</span>
+ {{ctrl.error}}
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelinesController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelinesController.js
new file mode 100644
index 000000000..a6f4258a2
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/guidelinesController.js
@@ -0,0 +1,322 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('testapiApp')
+ .controller('GuidelinesController', GuidelinesController);
+
+ GuidelinesController.$inject = ['$http', '$uibModal', 'testapiApiUrl'];
+
+ /**
+ * TestAPI Guidelines Controller
+ * This controller is for the '/guidelines' page where a user can browse
+ * through tests belonging to Interop WG defined capabilities.
+ */
+ function GuidelinesController($http, $uibModal, testapiApiUrl) {
+ var ctrl = this;
+
+ ctrl.getVersionList = getVersionList;
+ ctrl.update = update;
+ ctrl.updateTargetCapabilities = updateTargetCapabilities;
+ ctrl.filterStatus = filterStatus;
+ ctrl.getObjectLength = getObjectLength;
+ ctrl.openTestListModal = openTestListModal;
+
+ /** The target OpenStack marketing program to show capabilities for. */
+ ctrl.target = 'platform';
+
+ /** The various possible capability statuses. */
+ ctrl.status = {
+ required: true,
+ advisory: false,
+ deprecated: false,
+ removed: false
+ };
+
+ /**
+ * The template to load for displaying capability details.
+ */
+ ctrl.detailsTemplate = 'components/guidelines/partials/' +
+ 'guidelineDetails.html';
+
+ /**
+ * Retrieve an array of available guideline files from the TestAPI
+ * API server, sort this array reverse-alphabetically, and store it in
+ * a scoped variable. The scope's selected version is initialized to
+ * the latest (i.e. first) version here as well. After a successful API
+ * call, the function to update the capabilities is called.
+ * Sample API return array: ["2015.03.json", "2015.04.json"]
+ */
+ function getVersionList() {
+ var content_url = testapiApiUrl + '/guidelines';
+ ctrl.versionsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.versionList = data.sort().reverse();
+ // Default to the first approved guideline which is expected
+ // to be at index 1.
+ ctrl.version = ctrl.versionList[1];
+ ctrl.update();
+ }).error(function (error) {
+ ctrl.showError = true;
+ ctrl.error = 'Error retrieving version list: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This will contact the TestAPI API server to retrieve the JSON
+ * content of the guideline file corresponding to the selected
+ * version.
+ */
+ function update() {
+ var content_url = testapiApiUrl + '/guidelines/' + ctrl.version;
+ ctrl.capsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.guidelines = data;
+ ctrl.updateTargetCapabilities();
+ }).error(function (error) {
+ ctrl.showError = true;
+ ctrl.guidelines = null;
+ ctrl.error = 'Error retrieving guideline content: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This will update the scope's 'targetCapabilities' object with
+ * capabilities belonging to the selected OpenStack marketing program
+ * (programs typically correspond to 'components' in the Interop WG
+ * schema). Each capability will have its status mapped to it.
+ */
+ function updateTargetCapabilities() {
+ ctrl.targetCapabilities = {};
+ var components = ctrl.guidelines.components;
+ var targetCaps = ctrl.targetCapabilities;
+
+ // The 'platform' target is comprised of multiple components, so
+ // we need to get the capabilities belonging to each of its
+ // components.
+ if (ctrl.target === 'platform') {
+ var platform_components = ctrl.guidelines.platform.required;
+
+ // This will contain status priority values, where lower
+ // values mean higher priorities.
+ var statusMap = {
+ required: 1,
+ advisory: 2,
+ deprecated: 3,
+ removed: 4
+ };
+
+ // For each component required for the platform program.
+ angular.forEach(platform_components, function (component) {
+ // Get each capability list belonging to each status.
+ angular.forEach(components[component],
+ function (caps, status) {
+ // For each capability.
+ angular.forEach(caps, function(cap) {
+ // If the capability has already been added.
+ if (cap in targetCaps) {
+ // If the status priority value is less
+ // than the saved priority value, update
+ // the value.
+ if (statusMap[status] <
+ statusMap[targetCaps[cap]]) {
+ targetCaps[cap] = status;
+ }
+ }
+ else {
+ targetCaps[cap] = status;
+ }
+ });
+ });
+ });
+ }
+ else {
+ angular.forEach(components[ctrl.target],
+ function (caps, status) {
+ angular.forEach(caps, function(cap) {
+ targetCaps[cap] = status;
+ });
+ });
+ }
+ }
+
+ /**
+ * This filter will check if a capability's status corresponds
+ * to a status that is checked/selected in the UI. This filter
+ * is meant to be used with the ng-repeat directive.
+ * @param {Object} capability
+ * @returns {Boolean} True if capability's status is selected
+ */
+ function filterStatus(capability) {
+ var caps = ctrl.targetCapabilities;
+ return (ctrl.status.required &&
+ caps[capability.id] === 'required') ||
+ (ctrl.status.advisory &&
+ caps[capability.id] === 'advisory') ||
+ (ctrl.status.deprecated &&
+ caps[capability.id] === 'deprecated') ||
+ (ctrl.status.removed &&
+ caps[capability.id] === 'removed');
+ }
+
+ /**
+ * This function will get the length of an Object/dict based on
+ * the number of keys it has.
+ * @param {Object} object
+ * @returns {Number} length of object
+ */
+ function getObjectLength(object) {
+ return Object.keys(object).length;
+ }
+
+ /**
+ * This will open the modal that will show a list of all tests
+ * belonging to capabilities with the selected status(es).
+ */
+ function openTestListModal() {
+ $uibModal.open({
+ templateUrl: '/components/guidelines/partials' +
+ '/testListModal.html',
+ backdrop: true,
+ windowClass: 'modal',
+ animation: true,
+ controller: 'TestListModalController as modal',
+ size: 'lg',
+ resolve: {
+ version: function () {
+ return ctrl.version.slice(0, -5);
+ },
+ target: function () {
+ return ctrl.target;
+ },
+ status: function () {
+ return ctrl.status;
+ }
+ }
+ });
+ }
+
+ ctrl.getVersionList();
+ }
+
+ angular
+ .module('testapiApp')
+ .controller('TestListModalController', TestListModalController);
+
+ TestListModalController.$inject = [
+ '$uibModalInstance', '$http', 'version',
+ 'target', 'status', 'testapiApiUrl'
+ ];
+
+ /**
+ * Test List Modal Controller
+ * This controller is for the modal that appears if a user wants to see the
+ * test list corresponding to Interop WG capabilities with the selected
+ * statuses.
+ */
+ function TestListModalController($uibModalInstance, $http, version,
+ target, status, testapiApiUrl) {
+
+ var ctrl = this;
+
+ ctrl.version = version;
+ ctrl.target = target;
+ ctrl.status = status;
+ ctrl.close = close;
+ ctrl.updateTestListString = updateTestListString;
+
+ ctrl.aliases = true;
+ ctrl.flagged = false;
+
+ // Check if the API URL is absolute or relative.
+ if (testapiApiUrl.indexOf('http') > -1) {
+ ctrl.url = testapiApiUrl;
+ }
+ else {
+ ctrl.url = location.protocol + '//' + location.host +
+ testapiApiUrl;
+ }
+
+ /**
+ * This function will close/dismiss the modal.
+ */
+ function close() {
+ $uibModalInstance.dismiss('exit');
+ }
+
+ /**
+ * This function will return a list of statuses based on which ones
+ * are selected.
+ */
+ function getStatusList() {
+ var statusList = [];
+ angular.forEach(ctrl.status, function(value, key) {
+ if (value) {
+ statusList.push(key);
+ }
+ });
+ return statusList;
+ }
+
+ /**
+ * This will get the list of tests from the API and update the
+ * controller's test list string variable.
+ */
+ function updateTestListString() {
+ var statuses = getStatusList();
+ if (!statuses.length) {
+ ctrl.error = 'No tests matching selected criteria.';
+ return;
+ }
+ ctrl.testListUrl = [
+ ctrl.url, '/guidelines/', ctrl.version, '/tests?',
+ 'target=', ctrl.target, '&',
+ 'type=', statuses.join(','), '&',
+ 'alias=', ctrl.aliases.toString(), '&',
+ 'flag=', ctrl.flagged.toString()
+ ].join('');
+ ctrl.testListRequest =
+ $http.get(ctrl.testListUrl).
+ then(function successCallback(response) {
+ ctrl.error = null;
+ ctrl.testListString = response.data;
+ if (!ctrl.testListString) {
+ ctrl.testListCount = 0;
+ }
+ else {
+ ctrl.testListCount =
+ ctrl.testListString.split('\n').length;
+ }
+ }, function errorCallback(response) {
+ ctrl.testListString = null;
+ ctrl.testListCount = null;
+ if (angular.isObject(response.data) &&
+ response.data.message) {
+ ctrl.error = 'Error retrieving test list: ' +
+ response.data.message;
+ }
+ else {
+ ctrl.error = 'Unknown error retrieving test list.';
+ }
+ });
+ }
+
+ updateTestListString();
+ }
+})();
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/guidelineDetails.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/guidelineDetails.html
new file mode 100644
index 000000000..f020c9a09
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/guidelineDetails.html
@@ -0,0 +1,50 @@
+<!--
+HTML for guidelines page for all OpenStack Powered (TM) guideline schemas
+This expects the JSON data of the guidelines file to be stored in scope
+variable 'guidelines'.
+-->
+
+<ol ng-show="ctrl.guidelines" class="capabilities">
+ <li class="capability-list-item" ng-repeat="capability in ctrl.guidelines.capabilities | arrayConverter | filter:ctrl.filterStatus | orderBy:'id'">
+ <span class="capability-name">{{capability.id}}</span><br />
+ <em>{{capability.description}}</em><br />
+ Status: <span class="{{ctrl.targetCapabilities[capability.id]}}">{{ctrl.targetCapabilities[capability.id]}}</span><br />
+ <span ng-if="capability.project">Project: {{capability.project | capitalize}}<br /></span>
+ <a ng-click="showAchievements = !showAchievements">Achievements ({{capability.achievements.length}})</a><br />
+ <ol uib-collapse="!showAchievements" class="list-inline">
+ <li ng-repeat="achievement in capability.achievements">
+ {{achievement}}
+ </li>
+ </ol>
+
+ <a ng-click="showTests = !showTests">Tests ({{ctrl.getObjectLength(capability.tests)}})</a>
+ <ul uib-collapse="!showTests">
+ <li ng-if="ctrl.guidelines.schema === '1.2'" ng-repeat="test in capability.tests">
+ <span ng-class="{'glyphicon glyphicon-flag text-warning': capability.flagged.indexOf(test) > -1}"></span>
+ {{test}}
+ </li>
+ <li ng-if="ctrl.guidelines.schema > '1.2'" ng-repeat="(testName, testDetails) in capability.tests">
+ <span ng-class="{'glyphicon glyphicon-flag text-warning': testDetails.flagged}" title="{{testDetails.flagged.reason}}"></span>
+ {{testName}}
+ <div class="test-detail" ng-if="testDetails.aliases">
+ <strong>Aliases:</strong>
+ <ul><li ng-repeat="alias in testDetails.aliases">{{alias}}</li></ul>
+ </div>
+ </li>
+ </ul>
+ </li>
+</ol>
+
+<div ng-show="ctrl.guidelines" class="criteria">
+ <hr>
+ <h4><a ng-click="showCriteria = !showCriteria">Criteria</a></h4>
+ <div uib-collapse="showCriteria">
+ <ul>
+ <li ng-repeat="(key, criterion) in ctrl.guidelines.criteria">
+ <span class="criterion-name">{{criterion.name}}</span><br />
+ <em>{{criterion.Description}}</em><br />
+ Weight: {{criterion.weight}}
+ </li>
+ </ul>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/testListModal.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/testListModal.html
new file mode 100644
index 000000000..5b1d698d5
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/guidelines/partials/testListModal.html
@@ -0,0 +1,46 @@
+<div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" aria-hidden="true" ng-click="modal.close()">&times;</button>
+ <h4>Test List ({{modal.testListCount}})</h4>
+ <p>Use this test list with <a title="testapi-client" target="_blank"href="https://github.com/openstack/testapi-client">testapi-client</a>
+ to run only tests in the {{modal.version}} OpenStack Powered&#8482; guideline from capabilities with the following statuses:
+ </p>
+ <ul class="list-inline">
+ <li class="required" ng-if="modal.status.required"> Required</li>
+ <li class="advisory" ng-if="modal.status.advisory"> Advisory</li>
+ <li class="deprecated" ng-if="modal.status.deprecated"> Deprecated</li>
+ <li class="removed" ng-if="modal.status.removed"> Removed</li>
+ </ul>
+ <div class="checkbox checkbox-test-list">
+ <label><input type="checkbox" ng-model="modal.aliases" ng-change="modal.updateTestListString()">Aliases</label>
+ <span class="glyphicon glyphicon-info-sign info-hover" aria-hidden="true"
+ title="Include test aliases as tests may have been renamed over time. It does not hurt to include these."></span>
+ &nbsp;
+ <label><input type="checkbox" ng-model="modal.flagged" ng-change="modal.updateTestListString()">Flagged</label>
+ <span class="glyphicon glyphicon-info-sign info-hover" aria-hidden="true"
+ title="Include flagged tests.">
+ </span>
+ </div>
+ <p ng-hide="modal.error"> Alternatively, get the test list directly from the API on your CLI:</p>
+ <code ng-hide="modal.error">wget "{{modal.testListUrl}}" -O {{modal.version}}-test-list.txt</code>
+ </div>
+ <div class="modal-body tests-modal-content">
+ <div cg-busy="{promise:modal.testListRequest,message:'Loading'}"></div>
+ <div ng-show="modal.error" class="alert alert-danger" role="alert">
+ <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+ <span class="sr-only">Error:</span>
+ {{modal.error}}
+ </div>
+ <div class="form-group">
+ <textarea class="form-control" rows="16" id="tests" wrap="off">{{modal.testListString}}</textarea>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <a target="_blank" href="{{modal.testListUrl}}" download="{{modal.version + '-test-list.txt'}}">
+ <button class="btn btn-primary" ng-if="modal.testListCount > 0" type="button">
+ Download
+ </button>
+ </a>
+ <button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/home/home.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/home/home.html
new file mode 100644
index 000000000..04f64d52b
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/home/home.html
@@ -0,0 +1,23 @@
+<div class="jumbotron openstack-intro">
+ <div class="pull-right right openstack-intro__logo">
+ <img src="swagger/testapi-ui/assets/img/opnfv-logo.png" alt="OPNFV">
+ </div>
+ <div class="pull-left left openstack-intro__content">
+ <h1>Results Collection</h1>
+ <p>TestAPI is a source of tools for OPNFV test results collection</p>
+ </div>
+ <div class="clearfix"></div>
+</div>
+
+<div class="row">
+ <div class="col-lg-6">
+ <h4>What is TestAPI?</h4>
+ <ul>
+ <li>Toolset for testing interoperability between OPNFV test projects.</li>
+ <li>Database backed website supporting collection and publication of
+ community test results for OPNFV.</li>
+ <li>User interface to display individual test run results.</li>
+ </ul>
+ </div>
+</div>
+
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logout.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logout.html
new file mode 100644
index 000000000..38a5c3698
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logout.html
@@ -0,0 +1 @@
+<div cg-busy="{promise:ctrl.redirectWait,message:'Logging you out...'}"></div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logoutController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logoutController.js
new file mode 100644
index 000000000..1b6d78c63
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/logout/logoutController.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('testapiApp')
+ .controller('LogoutController', LogoutController);
+
+ LogoutController.$inject = [
+ '$location', '$window', '$timeout'
+ ];
+
+ /**
+ * TestAPI Logout Controller
+ * This controller handles logging out. In order to fully logout, the
+ * openstackid_session cookie must also be removed. The way to do that
+ * is to have the user's browser make a request to the openstackid logout
+ * page. We do this by placing the logout link as the src for an html
+ * image. After some time, the user is redirected home.
+ */
+ function LogoutController($location, $window, $timeout) {
+ var ctrl = this;
+
+ ctrl.openid_logout_url = $location.search().openid_logout;
+ var img = new Image(0, 0);
+ img.src = ctrl.openid_logout_url;
+ ctrl.redirectWait = $timeout(function() {
+ $window.location.href = '/';
+ }, 500);
+ }
+})();
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/importPubKeyModal.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/importPubKeyModal.html
new file mode 100644
index 000000000..0f55c27fd
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/importPubKeyModal.html
@@ -0,0 +1,27 @@
+<div class="modal-header">
+ <h4>Import Public Key</h4>
+ <p>Instructions for adding a public key and signature can be found
+ <a href="https://github.com/openstack/refstack/blob/master/doc/source/uploading_private_results.rst#generate-ssh-keys-locally"
+ target="_blank"
+ title="How to generate and upload SSH key and signature with testapi-client">here.
+ </a>
+ </p>
+</div>
+<div class="modal-body container-fluid">
+ <div class="row">
+ <div class="col-md-2">Public Key</div>
+ <div class="col-md-9 pull-right">
+ <textarea type="text" rows="10" cols="42" ng-model="modal.raw_key" required></textarea>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-2">Signature</div>
+ <div class="col-md-9 pull-right">
+ <textarea type="text" rows="10" cols="42" ng-model="modal.self_signature" required></textarea>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-warning btn-sm" ng-click="modal.cancel()">Cancel</button>
+ <button type="button" class="btn btn-default btn-sm" ng-click="modal.importPubKey()">Import Public Key</button>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profile.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profile.html
new file mode 100644
index 000000000..dc97c41e2
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profile.html
@@ -0,0 +1,37 @@
+<h3>User profile</h3>
+<div cg-busy="{promise:ctrl.authRequest,message:'Loading'}"></div>
+<div>
+ <table class="table table-striped table-hover">
+ <tbody>
+ <tr> <td>User name</td> <td>{{auth.currentUser.fullname}}</td> </tr>
+ <tr> <td>User OpenId</td> <td>{{auth.currentUser.openid}}</td> </tr>
+ <tr> <td>Email</td> <td>{{auth.currentUser.email}}</td> </tr>
+ </tbody>
+ </table>
+</div>
+<div ng-show="ctrl.pubkeys">
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col-md-4">
+ <h4>User Public Keys</h4>
+ </div>
+ <div class="col-md-2 pull-right">
+ <button type="button" class="btn btn-default btn-sm" ng-click="ctrl.openImportPubKeyModal()">
+ <span class="glyphicon glyphicon-plus"></span> Import Public Key
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <table class="table table-striped table-hover">
+ <tbody>
+ <tr ng-repeat="pubKey in ctrl.pubkeys" ng-click="ctrl.openShowPubKeyModal(pubKey)">
+ <td>{{pubKey.format}}</td>
+ <td>{{pubKey.shortKey}}</td>
+ <td>{{pubKey.comment}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profileController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profileController.js
new file mode 100644
index 000000000..0660e19f6
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/profileController.js
@@ -0,0 +1,219 @@
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('testapiApp')
+ .factory('PubKeys', PubKeys);
+
+ PubKeys.$inject = ['$resource', 'testapiApiUrl'];
+
+ /**
+ * This is a provider for the user's uploaded public keys.
+ */
+ function PubKeys($resource, testapiApiUrl) {
+ return $resource(testapiApiUrl + '/profile/pubkeys/:id', null, null);
+ }
+
+ angular
+ .module('testapiApp')
+ .controller('ProfileController', ProfileController);
+
+ ProfileController.$inject = [
+ '$scope', '$http', 'testapiApiUrl', 'PubKeys',
+ '$uibModal', 'raiseAlert', '$state'
+ ];
+
+ /**
+ * TestAPI Profile Controller
+ * This controller handles user's profile page, where a user can view
+ * account-specific information.
+ */
+ function ProfileController($scope, $http, testapiApiUrl,
+ PubKeys, $uibModal, raiseAlert, $state) {
+
+ var ctrl = this;
+
+ ctrl.updatePubKeys = updatePubKeys;
+ ctrl.openImportPubKeyModal = openImportPubKeyModal;
+ ctrl.openShowPubKeyModal = openShowPubKeyModal;
+
+ // Must be authenticated to view this page.
+ if (!$scope.auth.isAuthenticated) {
+ $state.go('home');
+ }
+
+ /**
+ * This function will fetch all the user's public keys from the
+ * server and store them in an array.
+ */
+ function updatePubKeys() {
+ var keys = PubKeys.query(function() {
+ ctrl.pubkeys = [];
+ angular.forEach(keys, function (key) {
+ ctrl.pubkeys.push({
+ 'resource': key,
+ 'format': key.format,
+ 'shortKey': [
+ key.pubkey.slice(0, 10),
+ '.',
+ key.pubkey.slice(-10)
+ ].join('.'),
+ 'pubkey': key.pubkey,
+ 'comment': key.comment
+ });
+ });
+ });
+ }
+
+ /**
+ * This function will open the modal that will give the user a form
+ * for importing a public key.
+ */
+ function openImportPubKeyModal() {
+ $uibModal.open({
+ templateUrl: '/components/profile/importPubKeyModal.html',
+ backdrop: true,
+ windowClass: 'modal',
+ controller: 'ImportPubKeyModalController as modal'
+ }).result.finally(function() {
+ ctrl.updatePubKeys();
+ });
+ }
+
+ /**
+ * This function will open the modal that will give the full
+ * information regarding a specific public key.
+ * @param {Object} pubKey resource
+ */
+ function openShowPubKeyModal(pubKey) {
+ $uibModal.open({
+ templateUrl: '/components/profile/showPubKeyModal.html',
+ backdrop: true,
+ windowClass: 'modal',
+ controller: 'ShowPubKeyModalController as modal',
+ resolve: {
+ pubKey: function() {
+ return pubKey;
+ }
+ }
+ }).result.finally(function() {
+ ctrl.updatePubKeys();
+ });
+ }
+
+ ctrl.authRequest = $scope.auth.doSignCheck().then(ctrl.updatePubKeys);
+ }
+
+ angular
+ .module('testapiApp')
+ .controller('ImportPubKeyModalController', ImportPubKeyModalController);
+
+ ImportPubKeyModalController.$inject = [
+ '$uibModalInstance', 'PubKeys', 'raiseAlert'
+ ];
+
+ /**
+ * Import Pub Key Modal Controller
+ * This controller is for the modal that appears if a user wants to import
+ * a public key.
+ */
+ function ImportPubKeyModalController($uibModalInstance,
+ PubKeys, raiseAlert) {
+
+ var ctrl = this;
+
+ ctrl.importPubKey = importPubKey;
+ ctrl.cancel = cancel;
+
+ /**
+ * This function will save a new public key resource to the API server.
+ */
+ function importPubKey() {
+ var newPubKey = new PubKeys(
+ {raw_key: ctrl.raw_key, self_signature: ctrl.self_signature}
+ );
+ newPubKey.$save(
+ function(newPubKey_) {
+ raiseAlert('success', '', 'Public key saved successfully');
+ $uibModalInstance.close(newPubKey_);
+ },
+ function(httpResp) {
+ raiseAlert('danger',
+ httpResp.statusText, httpResp.data.title);
+ ctrl.cancel();
+ }
+ );
+ }
+
+ /**
+ * This function will dismiss the modal.
+ */
+ function cancel() {
+ $uibModalInstance.dismiss('cancel');
+ }
+ }
+
+ angular
+ .module('testapiApp')
+ .controller('ShowPubKeyModalController', ShowPubKeyModalController);
+
+ ShowPubKeyModalController.$inject = [
+ '$uibModalInstance', 'raiseAlert', 'pubKey'
+ ];
+
+ /**
+ * Show Pub Key Modal Controller
+ * This controller is for the modal that appears if a user wants to see the
+ * full details of one of their public keys.
+ */
+ function ShowPubKeyModalController($uibModalInstance, raiseAlert, pubKey) {
+ var ctrl = this;
+
+ ctrl.deletePubKey = deletePubKey;
+ ctrl.cancel = cancel;
+
+ ctrl.pubKey = pubKey.resource;
+ ctrl.rawKey = [pubKey.format, pubKey.pubkey, pubKey.comment].join('\n');
+
+ /**
+ * This function will delete a public key resource.
+ */
+ function deletePubKey() {
+ ctrl.pubKey.$remove(
+ {id: ctrl.pubKey.id},
+ function() {
+ raiseAlert('success',
+ '', 'Public key deleted successfully');
+ $uibModalInstance.close(ctrl.pubKey.id);
+ },
+ function(httpResp) {
+ raiseAlert('danger',
+ httpResp.statusText, httpResp.data.title);
+ ctrl.cancel();
+ }
+ );
+ }
+
+ /**
+ * This method will dismiss the modal.
+ */
+ function cancel() {
+ $uibModalInstance.dismiss('cancel');
+ }
+ }
+})();
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/showPubKeyModal.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/showPubKeyModal.html
new file mode 100644
index 000000000..5f63a5ef6
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/profile/showPubKeyModal.html
@@ -0,0 +1,11 @@
+<div class="modal-header">
+ <h4>Public Key</h4>
+</div>
+<div class="modal-body container-fluid">
+ <textarea type="text" rows="10" cols="67" readonly="readonly">{{modal.rawKey}}</textarea>
+ <div class="modal-footer">
+ <button class="btn btn-warning" ng-click="modal.cancel()">Cancel</button>
+ <button type="button" class="btn btn-danger btn-sm" ng-click="modal.deletePubKey() "
+ confirm="Are you sure you want to delete this public key? You will lose management access to any test results signed with this key.">Delete</button>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html
new file mode 100644
index 000000000..583c9b92b
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html
@@ -0,0 +1,65 @@
+<div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" aria-hidden="true" ng-click="modal.close()">&times;</button>
+ <h4>Edit Test Run Metadata</h4>
+ <p>Make changes to your test metadata.</p>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <strong>Publicly Shared:</strong>
+ <select ng-model="modal.metaCopy.shared"
+ class="form-control">
+ <option value="true">Yes</option>
+ <option value="">No</option>
+ </select>
+ <br />
+ <strong>Associated Guideline:</strong>
+ <select ng-model="modal.metaCopy.guideline"
+ ng-options="o as o.slice(0, -5) for o in modal.versionList"
+ class="form-control">
+ <option value="">None</option>
+ </select>
+ <br />
+ <strong>Associated Target Program:</strong>
+ <select ng-model="modal.metaCopy.target"
+ class="form-control">
+ <option value="">None</option>
+ <option value="platform">OpenStack Powered Platform</option>
+ <option value="compute">OpenStack Powered Compute</option>
+ <option value="object">OpenStack Powered Object Storage</option>
+ </select>
+ <hr>
+ <strong>Associated Product:</strong>
+ <select ng-options="product as product.name for product in modal.products | arrayConverter | orderBy: 'name' track by product.id"
+ ng-model="modal.selectedProduct"
+ ng-change="modal.getProductVersions()"
+ class="form-control">
+ <option value="">-- No Product --</option>
+ </select>
+
+ <span ng-if="modal.productVersions.length">
+ <strong>Product Version:</strong>
+ <select ng-options="version as version.version for version in modal.productVersions | orderBy: 'version' track by version.id"
+ ng-model="modal.selectedVersion"
+ class="form-control">
+ </select>
+
+ </span>
+
+ </div>
+ <div ng-show="modal.showError" class="alert alert-danger" role="alert">
+ <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+ <span class="sr-only">Error:</span>
+ {{modal.error}}
+ </div>
+ <div ng-show="modal.showSuccess" class="alert alert-success" role="success">
+ <span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
+ <span class="sr-only">Success:</span>
+ Changes saved successfully.
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-primary" type="button" ng-click="modal.saveChanges()">Save Changes</button>
+ <button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html
new file mode 100644
index 000000000..6db198b02
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html
@@ -0,0 +1,13 @@
+<div class="modal-content">
+ <div class="modal-header">
+ <h4>All Passed Tests ({{modal.tests.length}})</h4>
+ </div>
+ <div class="modal-body tests-modal-content">
+ <div class="form-group">
+ <textarea class="form-control" rows="20" id="tests" wrap="off">{{modal.getTestListString()}}</textarea>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button>
+ </div>
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html
new file mode 100644
index 000000000..517e569c7
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html
@@ -0,0 +1,87 @@
+<!--
+HTML for each accordion group that separates the status types on the results
+report page.
+-->
+
+<uib-accordion-group is-open="isOpen" is-disabled="ctrl.caps[status].caps.length == 0">
+ <uib-accordion-heading>
+ {{status | capitalize}}
+ <small>
+ (<strong>Total:</strong> {{ctrl.caps[status].caps.length}} capabilities, {{ctrl.caps[status].count}} tests)
+ <span ng-if="ctrl.testStatus !== 'total'">
+ (<strong>{{ctrl.testStatus | capitalize}}:</strong> {{ctrl.getStatusTestCount(status)}} tests)
+ </span>
+ </small>
+ <i class="pull-right glyphicon"
+ ng-class="{'glyphicon-chevron-down': isOpen, 'glyphicon-chevron-right': !isOpen}">
+ </i>
+ </uib-accordion-heading>
+ <ol class="capabilities">
+ <li ng-repeat="capability in ctrl.caps[status].caps | orderBy:'id'"
+ ng-if="ctrl.isCapabilityShown(capability)">
+
+ <a ng-click="showTests = !showTests"
+ title="{{ctrl.guidelineData.capabilities[capability.id].description}}">
+ {{capability.id}}
+ </a>
+ <span ng-class="{'text-success': ctrl.testStatus === 'passed',
+ 'text-danger': ctrl.testStatus === 'not passed',
+ 'text-warning': ctrl.testStatus === 'flagged'}"
+ ng-if="ctrl.testStatus !== 'total'">
+ [{{ctrl.getCapabilityTestCount(capability)}}]
+ </span>
+ <span ng-class="{'text-success': (capability.passedTests.length > 0 &&
+ capability.notPassedTests.length == 0),
+ 'text-danger': (capability.passedTests.length == 0 &&
+ capability.notPassedTests.length > 0),
+ 'text-warning': (capability.passedTests.length > 0 &&
+ capability.notPassedTests.length > 0)}"
+ ng-if="ctrl.testStatus === 'total'">
+ [{{capability.passedTests.length}}/{{capability.passedTests.length +
+ capability.notPassedTests.length}}]
+ </span>
+
+ <ul class="list-unstyled" uib-collapse="!showTests">
+ <!-- Start passed test list -->
+ <li ng-repeat="test in capability.passedTests | orderBy:'toString()'"
+ ng-if="ctrl.isTestShown(test, capability)">
+
+ <span class="glyphicon glyphicon-ok text-success"
+ aria-hidden="true">
+ </span>
+ <span ng-class="{'glyphicon glyphicon-flag text-warning':
+ ctrl.isTestFlagged(test, ctrl.guidelineData.capabilities[capability.id])}"
+ title="{{ctrl.getFlaggedReason(test, ctrl.guidelineData.capabilities[capability.id])}}">
+ </span>
+ {{test}}
+ <span ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases"> &mdash;
+ <a ng-click="showAliases = !showAliases">[Aliases]</a>
+ <div class="test-detail-report" ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases && showAliases">
+ <ul><li ng-repeat="alias in ctrl.guidelineData.capabilities[capability.id].tests[test].aliases">{{alias}}</li></ul>
+ </div>
+ </span>
+ </li>
+ <!-- End passed test list -->
+
+ <!-- Start not passed test list -->
+ <li ng-repeat="test in capability.notPassedTests | orderBy:'toString()'"
+ ng-if="ctrl.isTestShown(test, capability)">
+
+ <span class="glyphicon glyphicon-remove text-danger" aria-hidden="true"></span>
+ <span ng-class="{'glyphicon glyphicon-flag text-warning':
+ ctrl.isTestFlagged(test, ctrl.guidelineData.capabilities[capability.id])}"
+ title="{{ctrl.getFlaggedReason(test, ctrl.guidelineData.capabilities[capability.id])}}">
+ </span>
+ {{test}}
+ <span ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases"> &mdash;
+ <a ng-click="showAliases = !showAliases">[Aliases]</a>
+ <div class="test-detail-report" ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases && showAliases">
+ <ul><li ng-repeat="alias in ctrl.guidelineData.capabilities[capability.id].tests[test].aliases">{{alias}}</li></ul>
+ </div>
+ </span>
+ </li>
+ <!-- End not passed test list -->
+ </ul>
+ </li>
+ </ol>
+</uib-accordion-group>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html
new file mode 100644
index 000000000..5527121ba
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html
@@ -0,0 +1,185 @@
+<h3>Test Run Results</h3>
+
+<div ng-show="ctrl.resultsData" class="container-fluid">
+ <div class="row">
+ <div class="pull-left">
+ <div class="test-report">
+ <strong>Test ID:</strong> {{ctrl.testId}}<br />
+ <div ng-if="ctrl.isResultAdmin()"><strong>Cloud ID:</strong> {{ctrl.resultsData.cpid}}<br /></div>
+ <strong>Upload Date:</strong> {{ctrl.resultsData.created_at}} UTC<br />
+ <strong>Duration:</strong> {{ctrl.resultsData.duration_seconds}} seconds<br />
+ <strong>Total Number of Passed Tests:</strong>
+ <a title="See all passed tests" ng-click="ctrl.openFullTestListModal()">
+ {{ctrl.resultsData.results.length}}
+ </a>
+ </div>
+ <hr>
+ <div ng-show="ctrl.isResultAdmin()">
+ <strong>Publicly Shared:</strong>
+ <span ng-if="ctrl.resultsData.meta.shared">Yes</span>
+ <span ng-if="!ctrl.resultsData.meta.shared">No</span>
+ <br />
+ </div>
+ <div ng-show="ctrl.resultsData.product_version">
+ <strong>Product:</strong>
+ {{ctrl.resultsData.product_version.product_info.name}}
+ <span ng-if="ctrl.resultsData.product_version.version">
+ ({{ctrl.resultsData.product_version.version}})
+ </span><br />
+ </div>
+ <div ng-show="ctrl.resultsData.meta.guideline">
+ <strong>Associated Guideline:</strong>
+ {{ctrl.resultsData.meta.guideline.slice(0, -5)}}
+ </div>
+ <div ng-show="ctrl.resultsData.meta.target">
+ <strong>Associated Target Program:</strong>
+ {{ctrl.targetMappings[ctrl.resultsData.meta.target]}}
+ </div>
+ <div ng-show="ctrl.resultsData.verification_status">
+ <strong>Verified:</strong>
+ <span class="yes">YES</span>
+ </div>
+ <hr>
+ </div>
+
+ <div class="pull-right">
+ <div ng-show="ctrl.isResultAdmin() && !ctrl.resultsData.verification_status">
+ <button class="btn btn-info" ng-click="ctrl.openEditTestModal()">Edit</button>
+ <button type="button" class="btn btn-danger" ng-click="ctrl.deleteTestRun()" confirm="Are you sure you want to delete these test run results?">Delete</button>
+ </div>
+ <div ng-show="ctrl.resultsData.user_role === 'foundation'">
+ <hr>
+ <div class="checkbox checkbox-verified">
+ <label><input type="checkbox"
+ ng-model="ctrl.isVerified"
+ ng-change="ctrl.updateVerificationStatus()"
+ ng-true-value="1"
+ ng-false-value="0">
+ <strong>Verified</strong>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div ng-show="ctrl.resultsData">
+ <p>See how these results stack up against Interop Working Group capabilities and OpenStack
+ <a target="_blank" href="http://www.openstack.org/brand/interop/">target marketing programs.</a>
+ </p>
+
+ <!-- User Options -->
+ <div class="row">
+ <div class="col-md-3">
+ <strong>Guideline Version:</strong>
+ <!-- Slicing the version file name here gets rid of the '.json' file extension -->
+ <select ng-model="ctrl.version"
+ ng-change="ctrl.updateGuidelines()"
+ class="form-control"
+ ng-options="versionFile.slice(0,-5) for versionFile in ctrl.versionList">
+ </select>
+ </div>
+ <div class="col-md-4">
+ <strong>Target Program:</strong>
+ <select ng-model="ctrl.target" class="form-control" ng-change="ctrl.buildCapabilitiesObject()">
+ <option value="platform">OpenStack Powered Platform</option>
+ <option value="compute">OpenStack Powered Compute</option>
+ <option value="object">OpenStack Powered Object Storage</option>
+ </select>
+ </div>
+ </div>
+ <!-- End User Options -->
+
+ <br />
+ <div ng-if="ctrl.guidelineData">
+ <strong>Guideline Status:</strong>
+ {{ctrl.guidelineData.status | capitalize}}
+ </div>
+
+ <strong>Corresponding OpenStack Releases:</strong>
+ <ul class="list-inline">
+ <li ng-repeat="release in ctrl.guidelineData.releases">
+ {{release | capitalize}}
+ </li>
+ </ul>
+ <hr >
+
+ <div ng-show="ctrl.guidelineData">
+ <strong>Status:</strong>
+ <p>This cloud passes <strong>{{ctrl.requiredPassPercent | number:1}}% </strong>
+ ({{ctrl.caps.required.passedCount}}/{{ctrl.caps.required.count}})
+ of the tests in the <strong>{{ctrl.version.slice(0, -5)}}</strong> <em>required</em> capabilities for the
+ <strong>{{ctrl.targetMappings[target]}}</strong> program. <br />
+ Excluding flagged tests, this cloud passes
+ <strong>{{ctrl.nonFlagRequiredPassPercent | number:1}}%</strong>
+ ({{ctrl.nonFlagPassCount}}/{{ctrl.totalNonFlagCount}})
+ of the <em>required</em> tests.
+ </p>
+
+ <p>Compliance with <strong>{{ctrl.version.slice(0, -5)}}</strong>:
+ <strong>
+ <span ng-if="ctrl.nonFlagPassCount === ctrl.totalNonFlagCount" class="yes">YES</span>
+ <span ng-if="ctrl.nonFlagPassCount !== ctrl.totalNonFlagCount" class="no">NO</span>
+ </strong>
+ </p>
+
+ <hr>
+ <h4>Capability Overview</h4>
+
+ Test Filters:<br />
+ <div class="btn-group button-margin" data-toggle="buttons">
+ <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'total'}">
+ <input type="radio" ng-model="ctrl.testStatus" value="total">
+ <span class="text-primary">All</span>
+ </label>
+ <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'passed'}">
+ <input type="radio" ng-model="ctrl.testStatus" value="passed">
+ <span class="text-success">Passed</span>
+ </label>
+ <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'not passed'}">
+ <input type="radio" ng-model="ctrl.testStatus" value="not passed">
+ <span class="text-danger">Not Passed</span>
+ </label>
+ <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'flagged'}">
+ <input type="radio" ng-model="ctrl.testStatus" value="flagged">
+ <span class="text-warning">Flagged</span>
+ </label>
+ </div>
+
+ <uib-accordion close-others=false>
+ <!-- The ng-repeat is used to pass in a local variable to the template. -->
+ <ng-include
+ ng-repeat="status in ['required']"
+ src="ctrl.detailsTemplate"
+ onload="isOpen = true">
+ </ng-include>
+ <br />
+ <ng-include
+ ng-repeat="status in ['advisory']"
+ src="ctrl.detailsTemplate">
+ </ng-include>
+ <br />
+ <ng-include
+ ng-repeat="status in ['deprecated']"
+ src="ctrl.detailsTemplate">
+ </ng-include>
+ <br />
+ <ng-include
+ ng-repeat="status in ['removed']"
+ src="ctrl.detailsTemplate">
+ </ng-include>
+ </uib-accordion>
+ </div>
+</div>
+
+<div class="loading">
+ <div cg-busy="{promise:versionsRequest,message:'Loading versions'}"></div>
+ <div cg-busy="{promise:capsRequest,message:'Loading capabilities'}"></div>
+ <div cg-busy="{promise:resultsRequest,message:'Loading results'}"></div>
+</div>
+
+<div ng-show="ctrl.showError" class="alert alert-danger" role="alert">
+ <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+ <span class="sr-only">Error:</span>
+ {{ctrl.error}}
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js
new file mode 100644
index 000000000..591ad402b
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js
@@ -0,0 +1,869 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('testapiApp')
+ .controller('ResultsReportController', ResultsReportController);
+
+ ResultsReportController.$inject = [
+ '$http', '$stateParams', '$window',
+ '$uibModal', 'testapiApiUrl', 'raiseAlert'
+ ];
+
+ /**
+ * TestAPI Results Report Controller
+ * This controller is for the '/results/<test run ID>' page where a user can
+ * view details for a specific test run.
+ */
+ function ResultsReportController($http, $stateParams, $window,
+ $uibModal, testapiApiUrl, raiseAlert) {
+
+ var ctrl = this;
+
+ ctrl.getVersionList = getVersionList;
+ ctrl.getResults = getResults;
+ ctrl.isResultAdmin = isResultAdmin;
+ ctrl.isShared = isShared;
+ ctrl.shareTestRun = shareTestRun;
+ ctrl.deleteTestRun = deleteTestRun;
+ ctrl.updateVerificationStatus = updateVerificationStatus;
+ ctrl.updateGuidelines = updateGuidelines;
+ ctrl.getTargetCapabilities = getTargetCapabilities;
+ ctrl.buildCapabilityV1_2 = buildCapabilityV1_2;
+ ctrl.buildCapabilityV1_3 = buildCapabilityV1_3;
+ ctrl.buildCapabilitiesObject = buildCapabilitiesObject;
+ ctrl.isTestFlagged = isTestFlagged;
+ ctrl.getFlaggedReason = getFlaggedReason;
+ ctrl.isCapabilityShown = isCapabilityShown;
+ ctrl.isTestShown = isTestShown;
+ ctrl.getCapabilityTestCount = getCapabilityTestCount;
+ ctrl.getStatusTestCount = getStatusTestCount;
+ ctrl.openFullTestListModal = openFullTestListModal;
+ ctrl.openEditTestModal = openEditTestModal;
+
+ /** The testID extracted from the URL route. */
+ ctrl.testId = $stateParams.testID;
+
+ /** The target OpenStack marketing program to compare against. */
+ ctrl.target = 'platform';
+
+ /** Mappings of Interop WG components to marketing program names. */
+ ctrl.targetMappings = {
+ 'platform': 'Openstack Powered Platform',
+ 'compute': 'OpenStack Powered Compute',
+ 'object': 'OpenStack Powered Object Storage'
+ };
+
+ /** The schema version of the currently selected guideline data. */
+ ctrl.schemaVersion = null;
+
+ /** The selected test status used for test filtering. */
+ ctrl.testStatus = 'total';
+
+ /** The HTML template that all accordian groups will use. */
+ ctrl.detailsTemplate = 'components/results-report/partials/' +
+ 'reportDetails.html';
+
+ /**
+ * Retrieve an array of available guideline files from the TestAPI
+ * API server, sort this array reverse-alphabetically, and store it in
+ * a scoped variable. The scope's selected version is initialized to
+ * the latest (i.e. first) version here as well. After a successful API
+ * call, the function to update the capabilities is called.
+ * Sample API return array: ["2015.03.json", "2015.04.json"]
+ */
+ function getVersionList() {
+ var content_url = testapiApiUrl + '/guidelines';
+ ctrl.versionsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.versionList = data.sort().reverse();
+ if (!ctrl.version) {
+ // Default to the first approved guideline which is
+ // expected to be at index 1.
+ ctrl.version = ctrl.versionList[1];
+ }
+ ctrl.updateGuidelines();
+ }).error(function (error) {
+ ctrl.showError = true;
+ ctrl.error = 'Error retrieving version list: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * Retrieve results from the TestAPI API server based on the test
+ * run id in the URL. This function is the first function that will
+ * be called from the controller. Upon successful retrieval of results,
+ * the function that gets the version list will be called.
+ */
+ function getResults() {
+ var content_url = testapiApiUrl + '/results/' + ctrl.testId;
+ ctrl.resultsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.resultsData = data;
+ ctrl.version = ctrl.resultsData.meta.guideline;
+ ctrl.isVerified = ctrl.resultsData.verification_status;
+ if (ctrl.resultsData.meta.target) {
+ ctrl.target = ctrl.resultsData.meta.target;
+ }
+ getVersionList();
+ }).error(function (error) {
+ ctrl.showError = true;
+ ctrl.resultsData = null;
+ ctrl.error = 'Error retrieving results from server: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This tells you whether the current user has administrative
+ * privileges for the test result.
+ * @returns {Boolean} true if the user has admin privileges.
+ */
+ function isResultAdmin() {
+ return Boolean(ctrl.resultsData &&
+ (ctrl.resultsData.user_role === 'owner' ||
+ ctrl.resultsData.user_role === 'foundation'));
+ }
+ /**
+ * This tells you whether the current results are shared with the
+ * community or not.
+ * @returns {Boolean} true if the results are shared
+ */
+ function isShared() {
+ return Boolean(ctrl.resultsData &&
+ 'shared' in ctrl.resultsData.meta);
+ }
+
+ /**
+ * This will send an API request in order to share or unshare the
+ * current results based on the passed in shareState.
+ * @param {Boolean} shareState - Whether to share or unshare results.
+ */
+ function shareTestRun(shareState) {
+ var content_url = [
+ testapiApiUrl, '/results/', ctrl.testId, '/meta/shared'
+ ].join('');
+ if (shareState) {
+ ctrl.shareRequest =
+ $http.post(content_url, 'true').success(function () {
+ ctrl.resultsData.meta.shared = 'true';
+ raiseAlert('success', '', 'Test run shared!');
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ } else {
+ ctrl.shareRequest =
+ $http.delete(content_url).success(function () {
+ delete ctrl.resultsData.meta.shared;
+ raiseAlert('success', '', 'Test run unshared!');
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+ }
+
+ /**
+ * This will send a request to the API to delete the current
+ * test results set.
+ */
+ function deleteTestRun() {
+ var content_url = [
+ testapiApiUrl, '/results/', ctrl.testId
+ ].join('');
+ ctrl.deleteRequest =
+ $http.delete(content_url).success(function () {
+ $window.history.back();
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+
+ /**
+ * This will send a request to the API to delete the current
+ * test results set.
+ */
+ function updateVerificationStatus() {
+ var content_url = [
+ testapiApiUrl, '/results/', ctrl.testId
+ ].join('');
+ var data = {'verification_status': ctrl.isVerified};
+ ctrl.updateRequest =
+ $http.put(content_url, data).success(
+ function () {
+ ctrl.resultsData.verification_status = ctrl.isVerified;
+ raiseAlert('success', '',
+ 'Verification status changed!');
+ }).error(function (error) {
+ ctrl.isVerified = ctrl.resultsData.verification_status;
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+
+ /**
+ * This will contact the TestAPI API server to retrieve the JSON
+ * content of the guideline file corresponding to the selected
+ * version. A function to construct an object from the capability
+ * data will be called upon successful retrieval.
+ */
+ function updateGuidelines() {
+ ctrl.guidelineData = null;
+ ctrl.showError = false;
+ var content_url = testapiApiUrl + '/guidelines/' +
+ ctrl.version;
+ ctrl.capsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.guidelineData = data;
+ ctrl.schemaVersion = data.schema;
+ ctrl.buildCapabilitiesObject();
+ }).error(function (error) {
+ ctrl.showError = true;
+ ctrl.guidelineData = null;
+ ctrl.error = 'Error retrieving guideline date: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This will get all the capabilities relevant to the target and
+ * their corresponding statuses.
+ * @returns {Object} Object containing each capability and their status
+ */
+ function getTargetCapabilities() {
+ var components = ctrl.guidelineData.components;
+ var targetCaps = {};
+
+ // The 'platform' target is comprised of multiple components, so
+ // we need to get the capabilities belonging to each of its
+ // components.
+ if (ctrl.target === 'platform') {
+ var platform_components =
+ ctrl.guidelineData.platform.required;
+
+ // This will contain status priority values, where lower
+ // values mean higher priorities.
+ var statusMap = {
+ required: 1,
+ advisory: 2,
+ deprecated: 3,
+ removed: 4
+ };
+
+ // For each component required for the platform program.
+ angular.forEach(platform_components, function (component) {
+ // Get each capability list belonging to each status.
+ angular.forEach(components[component],
+ function (caps, status) {
+ // For each capability.
+ angular.forEach(caps, function(cap) {
+ // If the capability has already been added.
+ if (cap in targetCaps) {
+ // If the status priority value is less
+ // than the saved priority value, update
+ // the value.
+ if (statusMap[status] <
+ statusMap[targetCaps[cap]]) {
+ targetCaps[cap] = status;
+ }
+ }
+ else {
+ targetCaps[cap] = status;
+ }
+ });
+ });
+ });
+ }
+ else {
+ angular.forEach(components[ctrl.target],
+ function (caps, status) {
+ angular.forEach(caps, function(cap) {
+ targetCaps[cap] = status;
+ });
+ });
+ }
+ return targetCaps;
+ }
+
+ /**
+ * This will build the a capability object for schema version 1.2.
+ * This object will contain the information needed to form a report in
+ * the HTML template.
+ * @param {String} capId capability ID
+ */
+ function buildCapabilityV1_2(capId) {
+ var cap = {
+ 'id': capId,
+ 'passedTests': [],
+ 'notPassedTests': [],
+ 'passedFlagged': [],
+ 'notPassedFlagged': []
+ };
+ var capDetails = ctrl.guidelineData.capabilities[capId];
+ // Loop through each test belonging to the capability.
+ angular.forEach(capDetails.tests,
+ function (testId) {
+ // If the test ID is in the results' test list, add
+ // it to the passedTests array.
+ if (ctrl.resultsData.results.indexOf(testId) > -1) {
+ cap.passedTests.push(testId);
+ if (capDetails.flagged.indexOf(testId) > -1) {
+ cap.passedFlagged.push(testId);
+ }
+ }
+ else {
+ cap.notPassedTests.push(testId);
+ if (capDetails.flagged.indexOf(testId) > -1) {
+ cap.notPassedFlagged.push(testId);
+ }
+ }
+ });
+ return cap;
+ }
+
+ /**
+ * This will build the a capability object for schema version 1.3 and
+ * above. This object will contain the information needed to form a
+ * report in the HTML template.
+ * @param {String} capId capability ID
+ */
+ function buildCapabilityV1_3(capId) {
+ var cap = {
+ 'id': capId,
+ 'passedTests': [],
+ 'notPassedTests': [],
+ 'passedFlagged': [],
+ 'notPassedFlagged': []
+ };
+
+ // For cases where a capability listed in components is not
+ // in the capabilities object.
+ if (!(capId in ctrl.guidelineData.capabilities)) {
+ return cap;
+ }
+
+ // Loop through each test belonging to the capability.
+ angular.forEach(ctrl.guidelineData.capabilities[capId].tests,
+ function (details, testId) {
+ var passed = false;
+
+ // If the test ID is in the results' test list.
+ if (ctrl.resultsData.results.indexOf(testId) > -1) {
+ passed = true;
+ }
+ else if ('aliases' in details) {
+ var len = details.aliases.length;
+ for (var i = 0; i < len; i++) {
+ var alias = details.aliases[i];
+ if (ctrl.resultsData.results.indexOf(alias) > -1) {
+ passed = true;
+ break;
+ }
+ }
+ }
+
+ // Add to correct array based on whether the test was
+ // passed or not.
+ if (passed) {
+ cap.passedTests.push(testId);
+ if ('flagged' in details) {
+ cap.passedFlagged.push(testId);
+ }
+ }
+ else {
+ cap.notPassedTests.push(testId);
+ if ('flagged' in details) {
+ cap.notPassedFlagged.push(testId);
+ }
+ }
+ });
+ return cap;
+ }
+
+ /**
+ * This will check the schema version of the current capabilities file,
+ * and will call the correct method to build an object based on the
+ * capability data retrieved from the TestAPI API server.
+ */
+ function buildCapabilitiesObject() {
+ // This is the object template where 'count' is the number of
+ // total tests that fall under the given status, and 'passedCount'
+ // is the number of tests passed. The 'caps' array will contain
+ // objects with details regarding each capability.
+ ctrl.caps = {
+ 'required': {'caps': [], 'count': 0, 'passedCount': 0,
+ 'flagFailCount': 0, 'flagPassCount': 0},
+ 'advisory': {'caps': [], 'count': 0, 'passedCount': 0,
+ 'flagFailCount': 0, 'flagPassCount': 0},
+ 'deprecated': {'caps': [], 'count': 0, 'passedCount': 0,
+ 'flagFailCount': 0, 'flagPassCount': 0},
+ 'removed': {'caps': [], 'count': 0, 'passedCount': 0,
+ 'flagFailCount': 0, 'flagPassCount': 0}
+ };
+
+ switch (ctrl.schemaVersion) {
+ case '1.2':
+ var capMethod = 'buildCapabilityV1_2';
+ break;
+ case '1.3':
+ case '1.4':
+ case '1.5':
+ case '1.6':
+ capMethod = 'buildCapabilityV1_3';
+ break;
+ default:
+ ctrl.showError = true;
+ ctrl.guidelineData = null;
+ ctrl.error = 'The schema version for the guideline ' +
+ 'file selected (' + ctrl.schemaVersion +
+ ') is currently not supported.';
+ return;
+ }
+
+ // Get test details for each relevant capability and store
+ // them in the scope's 'caps' object.
+ var targetCaps = ctrl.getTargetCapabilities();
+ angular.forEach(targetCaps, function(status, capId) {
+ var cap = ctrl[capMethod](capId);
+ ctrl.caps[status].count +=
+ cap.passedTests.length + cap.notPassedTests.length;
+ ctrl.caps[status].passedCount += cap.passedTests.length;
+ ctrl.caps[status].flagPassCount += cap.passedFlagged.length;
+ ctrl.caps[status].flagFailCount +=
+ cap.notPassedFlagged.length;
+ ctrl.caps[status].caps.push(cap);
+ });
+
+ ctrl.requiredPassPercent = (ctrl.caps.required.passedCount *
+ 100 / ctrl.caps.required.count);
+
+ ctrl.totalRequiredFailCount = ctrl.caps.required.count -
+ ctrl.caps.required.passedCount;
+ ctrl.totalRequiredFlagCount =
+ ctrl.caps.required.flagFailCount +
+ ctrl.caps.required.flagPassCount;
+ ctrl.totalNonFlagCount = ctrl.caps.required.count -
+ ctrl.totalRequiredFlagCount;
+ ctrl.nonFlagPassCount = ctrl.totalNonFlagCount -
+ (ctrl.totalRequiredFailCount -
+ ctrl.caps.required.flagFailCount);
+
+ ctrl.nonFlagRequiredPassPercent = (ctrl.nonFlagPassCount *
+ 100 / ctrl.totalNonFlagCount);
+ }
+
+ /**
+ * This will check if a given test is flagged.
+ * @param {String} test ID of the test to check
+ * @param {Object} capObj capability that test is under
+ * @returns {Boolean} truthy value if test is flagged
+ */
+ function isTestFlagged(test, capObj) {
+ if (!capObj) {
+ return false;
+ }
+ return (((ctrl.schemaVersion === '1.2') &&
+ (capObj.flagged.indexOf(test) > -1)) ||
+ ((ctrl.schemaVersion >= '1.3') &&
+ (capObj.tests[test].flagged)));
+ }
+
+ /**
+ * This will return the reason a test is flagged. An empty string
+ * will be returned if the passed in test is not flagged.
+ * @param {String} test ID of the test to check
+ * @param {String} capObj capability that test is under
+ * @returns {String} reason
+ */
+ function getFlaggedReason(test, capObj) {
+ if ((ctrl.schemaVersion === '1.2') &&
+ (ctrl.isTestFlagged(test, capObj))) {
+
+ // Return a generic message since schema 1.2 does not
+ // provide flag reasons.
+ return 'Interop Working Group has flagged this test.';
+ }
+ else if ((ctrl.schemaVersion >= '1.3') &&
+ (ctrl.isTestFlagged(test, capObj))) {
+
+ return capObj.tests[test].flagged.reason;
+ }
+ else {
+ return '';
+ }
+ }
+
+ /**
+ * This will check the if a capability should be shown based on the
+ * test filter selected. If a capability does not have any tests
+ * belonging under the given filter, it should not be shown.
+ * @param {Object} capability Built object for capability
+ * @returns {Boolean} true if capability should be shown
+ */
+ function isCapabilityShown(capability) {
+ return ((ctrl.testStatus === 'total') ||
+ (ctrl.testStatus === 'passed' &&
+ capability.passedTests.length > 0) ||
+ (ctrl.testStatus === 'not passed' &&
+ capability.notPassedTests.length > 0) ||
+ (ctrl.testStatus === 'flagged' &&
+ (capability.passedFlagged.length +
+ capability.notPassedFlagged.length > 0)));
+ }
+
+ /**
+ * This will check the if a test should be shown based on the test
+ * filter selected.
+ * @param {String} test ID of the test
+ * @param {Object} capability Built object for capability
+ * @return {Boolean} true if test should be shown
+ */
+ function isTestShown(test, capability) {
+ return ((ctrl.testStatus === 'total') ||
+ (ctrl.testStatus === 'passed' &&
+ capability.passedTests.indexOf(test) > -1) ||
+ (ctrl.testStatus === 'not passed' &&
+ capability.notPassedTests.indexOf(test) > -1) ||
+ (ctrl.testStatus === 'flagged' &&
+ (capability.passedFlagged.indexOf(test) > -1 ||
+ capability.notPassedFlagged.indexOf(test) > -1)));
+ }
+
+ /**
+ * This will give the number of tests belonging under the selected
+ * test filter for a given capability.
+ * @param {Object} capability Built object for capability
+ * @returns {Number} number of tests under filter
+ */
+ function getCapabilityTestCount(capability) {
+ if (ctrl.testStatus === 'total') {
+ return capability.passedTests.length +
+ capability.notPassedTests.length;
+ }
+ else if (ctrl.testStatus === 'passed') {
+ return capability.passedTests.length;
+ }
+ else if (ctrl.testStatus === 'not passed') {
+ return capability.notPassedTests.length;
+ }
+ else if (ctrl.testStatus === 'flagged') {
+ return capability.passedFlagged.length +
+ capability.notPassedFlagged.length;
+ }
+ else {
+ return 0;
+ }
+ }
+
+ /**
+ * This will give the number of tests belonging under the selected
+ * test filter for a given status.
+ * @param {String} capability status
+ * @returns {Number} number of tests for status under filter
+ */
+ function getStatusTestCount(status) {
+ if (!ctrl.caps) {
+ return -1;
+ }
+ else if (ctrl.testStatus === 'total') {
+ return ctrl.caps[status].count;
+ }
+ else if (ctrl.testStatus === 'passed') {
+ return ctrl.caps[status].passedCount;
+ }
+ else if (ctrl.testStatus === 'not passed') {
+ return ctrl.caps[status].count -
+ ctrl.caps[status].passedCount;
+ }
+ else if (ctrl.testStatus === 'flagged') {
+ return ctrl.caps[status].flagFailCount +
+ ctrl.caps[status].flagPassCount;
+ }
+ else {
+ return -1;
+ }
+ }
+
+ /**
+ * This will open the modal that will show the full list of passed
+ * tests for the current results.
+ */
+ function openFullTestListModal() {
+ $uibModal.open({
+ templateUrl: '/components/results-report/partials' +
+ '/fullTestListModal.html',
+ backdrop: true,
+ windowClass: 'modal',
+ animation: true,
+ controller: 'FullTestListModalController as modal',
+ size: 'lg',
+ resolve: {
+ tests: function () {
+ return ctrl.resultsData.results;
+ }
+ }
+ });
+ }
+
+ /**
+ * This will open the modal that will all a user to edit test run
+ * metadata.
+ */
+ function openEditTestModal() {
+ $uibModal.open({
+ templateUrl: '/components/results-report/partials' +
+ '/editTestModal.html',
+ backdrop: true,
+ windowClass: 'modal',
+ animation: true,
+ controller: 'EditTestModalController as modal',
+ size: 'lg',
+ resolve: {
+ resultsData: function () {
+ return ctrl.resultsData;
+ }
+ }
+ });
+ }
+
+ getResults();
+ }
+
+ angular
+ .module('testapiApp')
+ .controller('FullTestListModalController', FullTestListModalController);
+
+ FullTestListModalController.$inject = ['$uibModalInstance', 'tests'];
+
+ /**
+ * Full Test List Modal Controller
+ * This controller is for the modal that appears if a user wants to see the
+ * full list of passed tests on a report page.
+ */
+ function FullTestListModalController($uibModalInstance, tests) {
+ var ctrl = this;
+
+ ctrl.tests = tests;
+
+ /**
+ * This function will close/dismiss the modal.
+ */
+ ctrl.close = function () {
+ $uibModalInstance.dismiss('exit');
+ };
+
+ /**
+ * This function will return a string representing the sorted
+ * tests list separated by newlines.
+ */
+ ctrl.getTestListString = function () {
+ return ctrl.tests.sort().join('\n');
+ };
+ }
+
+ angular
+ .module('testapiApp')
+ .controller('EditTestModalController', EditTestModalController);
+
+ EditTestModalController.$inject = [
+ '$uibModalInstance', '$http', '$state', 'raiseAlert',
+ 'testapiApiUrl', 'resultsData'
+ ];
+
+ /**
+ * Edit Test Modal Controller
+ * This controller is for the modal that appears if a user wants to edit
+ * test run metadata.
+ */
+ function EditTestModalController($uibModalInstance, $http, $state,
+ raiseAlert, testapiApiUrl, resultsData) {
+
+ var ctrl = this;
+
+ ctrl.getVersionList = getVersionList;
+ ctrl.getUserProducts = getUserProducts;
+ ctrl.associateProductVersion = associateProductVersion;
+ ctrl.getProductVersions = getProductVersions;
+ ctrl.saveChanges = saveChanges;
+
+ ctrl.resultsData = resultsData;
+ ctrl.metaCopy = angular.copy(resultsData.meta);
+ ctrl.prodVersionCopy = angular.copy(resultsData.product_version);
+
+ ctrl.getVersionList();
+ ctrl.getUserProducts();
+
+ /**
+ * Retrieve an array of available capability files from the TestAPI
+ * API server, sort this array reverse-alphabetically, and store it in
+ * a scoped variable.
+ * Sample API return array: ["2015.03.json", "2015.04.json"]
+ */
+ function getVersionList() {
+ if (ctrl.versionList) {
+ return;
+ }
+ var content_url = testapiApiUrl + '/guidelines';
+ ctrl.versionsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.versionList = data.sort().reverse();
+ }).error(function (error) {
+ raiseAlert('danger', error.title,
+ 'Unable to retrieve version list');
+ });
+ }
+
+ /**
+ * Get products user has management rights to or all products depending
+ * on the passed in parameter value.
+ */
+ function getUserProducts() {
+ var contentUrl = testapiApiUrl + '/products';
+ ctrl.productsRequest =
+ $http.get(contentUrl).success(function (data) {
+ ctrl.products = {};
+ angular.forEach(data.products, function(prod) {
+ if (prod.can_manage) {
+ ctrl.products[prod.id] = prod;
+ }
+ });
+ if (ctrl.prodVersionCopy) {
+ ctrl.selectedProduct = ctrl.products[
+ ctrl.prodVersionCopy.product_info.id
+ ];
+ }
+ ctrl.getProductVersions();
+ }).error(function (error) {
+ ctrl.products = null;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving Products listing from server: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * Send a PUT request to the API server to associate a product with
+ * a test result.
+ */
+ function associateProductVersion() {
+ var verId = (ctrl.selectedVersion ?
+ ctrl.selectedVersion.id : null);
+ var testId = resultsData.id;
+ var url = testapiApiUrl + '/results/' + testId;
+ ctrl.associateRequest = $http.put(url, {'product_version_id':
+ verId})
+ .error(function (error) {
+ ctrl.showError = true;
+ ctrl.showSuccess = false;
+ ctrl.error =
+ 'Error associating product version with test run: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * Get all versions for a product.
+ */
+ function getProductVersions() {
+ if (!ctrl.selectedProduct) {
+ ctrl.productVersions = [];
+ ctrl.selectedVersion = null;
+ return;
+ }
+
+ var url = testapiApiUrl + '/products/' +
+ ctrl.selectedProduct.id + '/versions';
+ ctrl.getVersionsRequest = $http.get(url)
+ .success(function (data) {
+ ctrl.productVersions = data;
+ if (ctrl.prodVersionCopy &&
+ ctrl.prodVersionCopy.product_info.id ==
+ ctrl.selectedProduct.id) {
+ ctrl.selectedVersion = ctrl.prodVersionCopy;
+ }
+ else {
+ angular.forEach(data, function(ver) {
+ if (!ver.version) {
+ ctrl.selectedVersion = ver;
+ }
+ });
+ }
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+
+ /**
+ * Send a PUT request to the server with the changes.
+ */
+ function saveChanges() {
+ ctrl.showError = false;
+ ctrl.showSuccess = false;
+ var metaBaseUrl = [
+ testapiApiUrl, '/results/', resultsData.id, '/meta/'
+ ].join('');
+ var metaFields = ['target', 'guideline', 'shared'];
+ var meta = ctrl.metaCopy;
+ angular.forEach(metaFields, function(field) {
+ var oldMetaValue = (field in ctrl.resultsData.meta) ?
+ ctrl.resultsData.meta[field] : '';
+ if (field in meta && oldMetaValue != meta[field]) {
+ var metaUrl = metaBaseUrl + field;
+ if (meta[field]) {
+ ctrl.assocRequest = $http.post(metaUrl, meta[field])
+ .success(function(data) {
+ ctrl.resultsData.meta[field] = meta[field];
+ })
+ .error(function (error) {
+ ctrl.showError = true;
+ ctrl.showSuccess = false;
+ ctrl.error =
+ 'Error associating metadata with ' +
+ 'test run: ' + angular.toJson(error);
+ });
+ }
+ else {
+ ctrl.unassocRequest = $http.delete(metaUrl)
+ .success(function (data) {
+ delete ctrl.resultsData.meta[field];
+ delete meta[field];
+ })
+ .error(function (error) {
+ ctrl.showError = true;
+ ctrl.showSuccess = false;
+ ctrl.error =
+ 'Error associating metadata with ' +
+ 'test run: ' + angular.toJson(error);
+ });
+ }
+ }
+ });
+ ctrl.associateProductVersion();
+ if (!ctrl.showError) {
+ ctrl.showSuccess = true;
+ $state.reload();
+ }
+ }
+
+ /**
+ * This function will close/dismiss the modal.
+ */
+ ctrl.close = function () {
+ $uibModalInstance.dismiss('exit');
+ };
+ }
+})();
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results/results.html b/utils/test/testapi/3rd_party/static/testapi-ui/components/results/results.html
new file mode 100644
index 000000000..2a43cd1e2
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results/results.html
@@ -0,0 +1,247 @@
+<h3>{{ctrl.pageHeader}}</h3>
+<p>{{ctrl.pageParagraph}}</p>
+
+<div class="result-filters">
+ <h4>Filters</h4>
+ <div class="row">
+ <div class="col-md-3">
+ <label for="cpid">Start Date</label>
+ <p class="input-group">
+ <input type="text" class="form-control"
+ uib-datepicker-popup="{{ctrl.format}}"
+ ng-model="ctrl.startDate" is-open="ctrl.startOpen"
+ close-text="Close" />
+ <span class="input-group-btn">
+ <button type="button" class="btn btn-default" ng-click="ctrl.open($event, 'startOpen')">
+ <i class="glyphicon glyphicon-calendar"></i>
+ </button>
+ </span>
+ </p>
+ </div>
+ <div class="col-md-3">
+ <label for="cpid">End Date</label>
+ <p class="input-group">
+ <input type="text" class="form-control"
+ uib-datepicker-popup="{{ctrl.format}}"
+ ng-model="ctrl.endDate" is-open="ctrl.endOpen"
+ close-text="Close" />
+ <span class="input-group-btn">
+ <button type="button" class="btn btn-default" ng-click="ctrl.open($event, 'endOpen')">
+ <i class="glyphicon glyphicon-calendar"></i>
+ </button>
+ </span>
+ </p>
+ </div>
+ <div class="col-md-3" style="margin-top:24px;">
+ <button type="submit" class="btn btn-primary" ng-click="ctrl.update()">Filter</button>
+ <button type="submit" class="btn btn-primary btn-danger" ng-click="ctrl.clearFilters()">Clear</button>
+ </div>
+ </div>
+</div>
+
+<div cg-busy="{promise:ctrl.authRequest,message:'Loading'}"></div>
+<div cg-busy="{promise:ctrl.resultsRequest,message:'Loading'}"></div>
+
+<div ng-show="ctrl.data" class="results-table">
+ <table ng-show="ctrl.data" class="table table-striped table-hover">
+ <thead>
+ <tr>
+ <th ng-if="ctrl.isUserResults"></th>
+ <th>Upload Date</th>
+ <th>Test Run ID</th>
+ <th ng-if="ctrl.isUserResults">Vendor</th>
+ <th ng-if="ctrl.isUserResults">Product (version)</th>
+ <th ng-if="ctrl.isUserResults">Target Program</th>
+ <th ng-if="ctrl.isUserResults">Guideline</th>
+ <th ng-if="ctrl.isUserResults">Verified</th>
+ <th ng-if="ctrl.isUserResults">Shared</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr ng-repeat-start="(index, result) in ctrl.data.results">
+ <td ng-if="ctrl.isUserResults">
+ <a ng-if="!result.expanded"
+ class="glyphicon glyphicon-plus"
+ ng-click="result.expanded = true">
+ </a>
+ <a ng-if="result.expanded"
+ class="glyphicon glyphicon-minus"
+ ng-click="result.expanded = false">
+ </a>
+ </td>
+ <td>{{result.created_at}}</td>
+ <td><a ui-sref="resultsDetail({testID: result.id})">
+ {{result.id.slice(0, 8)}}...{{result.id.slice(-8)}}
+ </a>
+ </td>
+ <td ng-if="ctrl.isUserResults">
+ {{ctrl.vendors[result.product_version.product_info.organization_id].name || '-'}}
+ </td>
+ <td ng-if="ctrl.isUserResults">{{result.product_version.product_info.name || '-'}}
+ <span ng-if="result.product_version.version">
+ ({{result.product_version.version}})
+ </span>
+ </td>
+ <td ng-if="ctrl.isUserResults">{{ctrl.targetMappings[result.meta.target] || '-'}}</td>
+ <td ng-if="ctrl.isUserResults">{{result.meta.guideline.slice(0, -5) || '-'}}</td>
+ <td ng-if="ctrl.isUserResults">
+ <span ng-if="result.verification_status" class="glyphicon glyphicon-ok"></span>
+ <span ng-if="!result.verification_status">-</span>
+
+ </td>
+ <td ng-if="ctrl.isUserResults">
+ <span ng-show="result.meta.shared" class="glyphicon glyphicon-share"></span>
+ </td>
+ </tr>
+ <tr ng-if="result.expanded" ng-repeat-end>
+ <td></td>
+ <td colspan="3">
+ <strong>Publicly Shared:</strong>
+ <span ng-if="result.meta.shared == 'true' && !result.sharedEdit">Yes</span>
+ <span ng-if="!result.meta.shared && !result.sharedEdit">
+ <em>No</em>
+ </span>
+ <select ng-if="result.sharedEdit"
+ ng-model="result.meta.shared"
+ class="form-inline">
+ <option value="true">Yes</option>
+ <option value="">No</option>
+ </select>
+ <a ng-if="!result.sharedEdit"
+ ng-click="result.sharedEdit = true"
+ title="Edit"
+ class="glyphicon glyphicon-pencil"></a>
+ <a ng-if="result.sharedEdit"
+ ng-click="ctrl.associateMeta(index,'shared',result.meta.shared)"
+ title="Save"
+ class="glyphicon glyphicon-floppy-disk"></a>
+ <br />
+
+ <strong>Associated Guideline:</strong>
+ <span ng-if="!result.meta.guideline && !result.guidelineEdit">
+ <em>None</em>
+ </span>
+ <span ng-if="result.meta.guideline && !result.guidelineEdit">
+ {{result.meta.guideline.slice(0, -5)}}
+ </span>
+ <select ng-if="result.guidelineEdit"
+ ng-model="result.meta.guideline"
+ ng-options="o as o.slice(0, -5) for o in ctrl.versionList"
+ class="form-inline">
+ <option value="">None</option>
+ </select>
+ <a ng-if="!result.guidelineEdit"
+ ng-click="ctrl.getVersionList();result.guidelineEdit = true"
+ title="Edit"
+ class="glyphicon glyphicon-pencil"></a>
+ <a ng-if="result.guidelineEdit"
+ ng-click="ctrl.associateMeta(index, 'guideline', result.meta.guideline)"
+ title="Save"
+ class="glyphicon glyphicon-floppy-disk">
+ </a>
+ <br />
+
+ <strong>Associated Target Program:</strong>
+ <span ng-if="!result.meta.target && !result.targetEdit">
+ <em>None</em>
+ </span>
+ <span ng-if="result.meta.target && !result.targetEdit">
+ {{ctrl.targetMappings[result.meta.target]}}</span>
+ <select ng-if="result.targetEdit"
+ ng-model="result.meta.target"
+ class="form-inline">
+ <option value="">None</option>
+ <option value="platform">OpenStack Powered Platform</option>
+ <option value="compute">OpenStack Powered Compute</option>
+ <option value="object">OpenStack Powered Object Storage</option>
+ </select>
+ <a ng-if="!result.targetEdit"
+ ng-click="result.targetEdit = true;"
+ title="Edit"
+ class="glyphicon glyphicon-pencil">
+ </a>
+ <a ng-if="result.targetEdit"
+ ng-click="ctrl.associateMeta(index, 'target', result.meta.target)"
+ title="Save"
+ class="glyphicon glyphicon-floppy-disk">
+ </a>
+ <br />
+
+ <strong>Associated Product:</strong>
+ <span ng-if="!result.product_version && !result.productEdit">
+ <em>None</em>
+ </span>
+ <span ng-if="result.product_version && !result.productEdit">
+ <span ng-if="ctrl.products[result.product_version.product_info.id].product_type == 0">
+ <a ui-sref="distro({id: result.product_version.product_info.id})">
+ {{ctrl.products[result.product_version.product_info.id].name}}
+ <small ng-if="result.product_version.version">
+ ({{result.product_version.version}})
+ </small>
+ </a>
+ </span>
+ <span ng-if="ctrl.products[result.product_version.product_info.id].product_type != 0">
+ <a ui-sref="cloud({id: result.product_version.product_info.id})">
+ {{ctrl.products[result.product_version.product_info.id].name}}
+ <small ng-if="result.product_version.version">
+ ({{result.product_version.version}})
+ </small>
+ </a>
+ </span>
+ </span>
+
+ <select ng-if="result.productEdit"
+ ng-options="product as product.name for product in ctrl.products | arrayConverter | orderBy: 'name' track by product.id"
+ ng-model="result.selectedProduct"
+ ng-change="ctrl.getProductVersions(result)">
+ <option value="">-- No Product --</option>
+ </select>
+
+ <span ng-if="result.productVersions.length && result.productEdit">
+ <span class="glyphicon glyphicon-arrow-right" style="padding-right:3px;color:#303030;"></span>
+ Version:
+ <select ng-options="version as version.version for version in result.productVersions | orderBy: 'version' track by version.id"
+ ng-model="result.selectedVersion">
+ </select>
+
+ </span>
+ <a ng-if="!result.productEdit"
+ ng-click="ctrl.prepVersionEdit(result)"
+ title="Edit"
+ class="glyphicon glyphicon-pencil">
+ </a>
+ <a ng-if="result.productEdit"
+ ng-click="ctrl.associateProductVersion(result)"
+ confirm="Once you associate this test to this product, ownership
+ will be transferred to the product's vendor admins.
+ Continue?"
+ title="Save"
+ class="glyphicon glyphicon-floppy-disk">
+ </a>
+ <br />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="pages">
+ <uib-pagination
+ total-items="ctrl.totalItems"
+ ng-model="ctrl.currentPage"
+ items-per-page="ctrl.itemsPerPage"
+ max-size="ctrl.maxSize"
+ class="pagination-sm"
+ boundary-links="true"
+ rotate="false"
+ num-pages="ctrl.numPages"
+ ng-change="ctrl.update()">
+ </uib-pagination>
+ </div>
+</div>
+
+<div ng-show="ctrl.showError" class="alert alert-danger" role="alert">
+ <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+ <span class="sr-only">Error:</span>
+ {{ctrl.error}}
+</div>
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js
new file mode 100644
index 000000000..2b0338c87
--- /dev/null
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js
@@ -0,0 +1,339 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('testapiApp')
+ .controller('ResultsController', ResultsController);
+
+ ResultsController.$inject = [
+ '$scope', '$http', '$filter', '$state', 'testapiApiUrl','raiseAlert'
+ ];
+
+ /**
+ * TestAPI Results Controller
+ * This controller is for the '/results' page where a user can browse
+ * a listing of community uploaded results.
+ */
+ function ResultsController($scope, $http, $filter, $state, testapiApiUrl,
+ raiseAlert) {
+ var ctrl = this;
+
+ ctrl.update = update;
+ ctrl.open = open;
+ ctrl.clearFilters = clearFilters;
+ ctrl.associateMeta = associateMeta;
+ ctrl.getVersionList = getVersionList;
+ ctrl.getUserProducts = getUserProducts;
+ ctrl.getVendors = getVendors;
+ ctrl.associateProductVersion = associateProductVersion;
+ ctrl.getProductVersions = getProductVersions;
+ ctrl.prepVersionEdit = prepVersionEdit;
+
+ /** Mappings of Interop WG components to marketing program names. */
+ ctrl.targetMappings = {
+ 'platform': 'Openstack Powered Platform',
+ 'compute': 'OpenStack Powered Compute',
+ 'object': 'OpenStack Powered Object Storage'
+ };
+
+ /** Initial page to be on. */
+ ctrl.currentPage = 1;
+
+ /**
+ * How many results should display on each page. Since pagination
+ * is server-side implemented, this value should match the
+ * 'results_per_page' configuration of the TestAPI server which
+ * defaults to 20.
+ */
+ ctrl.itemsPerPage = 20;
+
+ /**
+ * How many page buttons should be displayed at max before adding
+ * the '...' button.
+ */
+ ctrl.maxSize = 5;
+
+ /** The upload date lower limit to be used in filtering results. */
+ ctrl.startDate = '';
+
+ /** The upload date upper limit to be used in filtering results. */
+ ctrl.endDate = '';
+
+ /** The date format for the date picker. */
+ ctrl.format = 'yyyy-MM-dd';
+
+ /** Check to see if this page should display user-specific results. */
+ ctrl.isUserResults = $state.current.name === 'userResults';
+
+ // Should only be on user-results-page if authenticated.
+ if (ctrl.isUserResults && !$scope.auth.isAuthenticated) {
+ $state.go('home');
+ }
+
+ ctrl.pageHeader = ctrl.isUserResults ?
+ 'Private test results' : 'Community test results';
+
+ ctrl.pageParagraph = ctrl.isUserResults ?
+ 'Your most recently uploaded test results are listed here.' :
+ 'The most recently uploaded community test results are listed ' +
+ 'here.';
+
+ if (ctrl.isUserResults) {
+ ctrl.authRequest = $scope.auth.doSignCheck()
+ .then(ctrl.update);
+ ctrl.getUserProducts();
+ } else {
+ ctrl.update();
+ }
+
+ ctrl.getVendors();
+
+ /**
+ * This will contact the TestAPI API to get a listing of test run
+ * results.
+ */
+ function update() {
+ ctrl.showError = false;
+ // Construct the API URL based on user-specified filters.
+ var content_url = testapiApiUrl + '/results' +
+ '?page=' + ctrl.currentPage;
+ var start = $filter('date')(ctrl.startDate, 'yyyy-MM-dd');
+ if (start) {
+ content_url =
+ content_url + '&start_date=' + start + ' 00:00:00';
+ }
+ var end = $filter('date')(ctrl.endDate, 'yyyy-MM-dd');
+ if (end) {
+ content_url = content_url + '&end_date=' + end + ' 23:59:59';
+ }
+ if (ctrl.isUserResults) {
+ content_url = content_url + '&signed';
+ }
+ ctrl.resultsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.data = data;
+ ctrl.totalItems = ctrl.data.pagination.total_pages *
+ ctrl.itemsPerPage;
+ ctrl.currentPage = ctrl.data.pagination.current_page;
+ }).error(function (error) {
+ ctrl.data = null;
+ ctrl.totalItems = 0;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving results listing from server: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This is called when the date filter calendar is opened. It
+ * does some event handling, and sets a scope variable so the UI
+ * knows which calendar was opened.
+ * @param {Object} $event - The Event object
+ * @param {String} openVar - Tells which calendar was opened
+ */
+ function open($event, openVar) {
+ $event.preventDefault();
+ $event.stopPropagation();
+ ctrl[openVar] = true;
+ }
+
+ /**
+ * This function will clear all filters and update the results
+ * listing.
+ */
+ function clearFilters() {
+ ctrl.startDate = null;
+ ctrl.endDate = null;
+ ctrl.update();
+ }
+
+ /**
+ * This will send an API request in order to associate a metadata
+ * key-value pair with the given testId
+ * @param {Number} index - index of the test object in the results list
+ * @param {String} key - metadata key
+ * @param {String} value - metadata value
+ */
+ function associateMeta(index, key, value) {
+ var testId = ctrl.data.results[index].id;
+ var metaUrl = [
+ testapiApiUrl, '/results/', testId, '/meta/', key
+ ].join('');
+
+ var editFlag = key + 'Edit';
+ if (value) {
+ ctrl.associateRequest = $http.post(metaUrl, value)
+ .success(function () {
+ ctrl.data.results[index][editFlag] = false;
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+ else {
+ ctrl.unassociateRequest = $http.delete(metaUrl)
+ .success(function () {
+ ctrl.data.results[index][editFlag] = false;
+ }).error(function (error) {
+ if (error.code == 404) {
+ // Key doesn't exist, so count it as a success,
+ // and don't raise an alert.
+ ctrl.data.results[index][editFlag] = false;
+ }
+ else {
+ raiseAlert('danger', error.title, error.detail);
+ }
+ });
+ }
+ }
+
+ /**
+ * Retrieve an array of available capability files from the TestAPI
+ * API server, sort this array reverse-alphabetically, and store it in
+ * a scoped variable.
+ * Sample API return array: ["2015.03.json", "2015.04.json"]
+ */
+ function getVersionList() {
+ if (ctrl.versionList) {
+ return;
+ }
+ var content_url = testapiApiUrl + '/guidelines';
+ ctrl.versionsRequest =
+ $http.get(content_url).success(function (data) {
+ ctrl.versionList = data.sort().reverse();
+ }).error(function (error) {
+ raiseAlert('danger', error.title,
+ 'Unable to retrieve version list');
+ });
+ }
+
+ /**
+ * Get products user has management rights to or all products depending
+ * on the passed in parameter value.
+ */
+ function getUserProducts() {
+ if (ctrl.products) {
+ return;
+ }
+ var contentUrl = testapiApiUrl + '/products';
+ ctrl.productsRequest =
+ $http.get(contentUrl).success(function (data) {
+ ctrl.products = {};
+ angular.forEach(data.products, function(prod) {
+ if (prod.can_manage) {
+ ctrl.products[prod.id] = prod;
+ }
+ });
+ }).error(function (error) {
+ ctrl.products = null;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving Products listing from server: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This will contact the TestAPI API to get a listing of
+ * vendors.
+ */
+ function getVendors() {
+ var contentUrl = testapiApiUrl + '/vendors';
+ ctrl.vendorsRequest =
+ $http.get(contentUrl).success(function (data) {
+ ctrl.vendors = {};
+ data.vendors.forEach(function(vendor) {
+ ctrl.vendors[vendor.id] = vendor;
+ });
+ }).error(function (error) {
+ ctrl.vendors = null;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving vendor listing from server: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * Send a PUT request to the API server to associate a product with
+ * a test result.
+ */
+ function associateProductVersion(result) {
+ var verId = (result.selectedVersion ?
+ result.selectedVersion.id : null);
+ var testId = result.id;
+ var url = testapiApiUrl + '/results/' + testId;
+ ctrl.associateRequest = $http.put(url, {'product_version_id':
+ verId})
+ .success(function (data) {
+ result.product_version = result.selectedVersion;
+ if (result.selectedVersion) {
+ result.product_version.product_info =
+ result.selectedProduct;
+ }
+ result.productEdit = false;
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+
+ /**
+ * Get all versions for a product.
+ */
+ function getProductVersions(result) {
+ if (!result.selectedProduct) {
+ result.productVersions = [];
+ result.selectedVersion = null;
+ return;
+ }
+
+ var url = testapiApiUrl + '/products/' +
+ result.selectedProduct.id + '/versions';
+ ctrl.getVersionsRequest = $http.get(url)
+ .success(function (data) {
+ result.productVersions = data;
+
+ // If the test result isn't already associated to a
+ // version, default it to the null version.
+ if (!result.product_version) {
+ angular.forEach(data, function(ver) {
+ if (!ver.version) {
+ result.selectedVersion = ver;
+ }
+ });
+ }
+ }).error(function (error) {
+ raiseAlert('danger', error.title, error.detail);
+ });
+ }
+
+ /**
+ * Instantiate variables needed for editing product/version
+ * associations.
+ */
+ function prepVersionEdit(result) {
+ result.productEdit = true;
+ if (result.product_version) {
+ result.selectedProduct =
+ ctrl.products[result.product_version.product_info.id];
+ }
+ result.selectedVersion = result.product_version;
+ ctrl.getProductVersions(result);
+ }
+
+ }
+})();