summaryrefslogtreecommitdiffstats
path: root/testapi/opnfv_testapi/ui
diff options
context:
space:
mode:
authorSerenaFeng <feng.xiaowei@zte.com.cn>2017-10-27 16:58:49 +0800
committerSerena Feng <feng.xiaowei@zte.com.cn>2017-10-31 01:00:35 +0000
commit426aa0231af6cb1911ac7b6fbd36062e338af05a (patch)
tree26199aa442137d9783feb568bf07b57bc5829470 /testapi/opnfv_testapi/ui
parent529b408c84abb20e3ac28f5d07fe485d9f55ac12 (diff)
remove components out side of 3rd_party
components are implemented by TestAPI team, they are not the third party code, put them under opnfv_testapi/ui directory Change-Id: Ia0f5e2afe4bcb12bcb74d3a8d78fb28fe1432bec Signed-off-by: SerenaFeng <feng.xiaowei@zte.com.cn>
Diffstat (limited to 'testapi/opnfv_testapi/ui')
-rw-r--r--testapi/opnfv_testapi/ui/about/about.html32
-rw-r--r--testapi/opnfv_testapi/ui/auth-failure/authFailureController.js33
-rw-r--r--testapi/opnfv_testapi/ui/home/home.html23
-rw-r--r--testapi/opnfv_testapi/ui/logout/logout.html1
-rw-r--r--testapi/opnfv_testapi/ui/logout/logoutController.js44
-rw-r--r--testapi/opnfv_testapi/ui/pods/pods.html76
-rw-r--r--testapi/opnfv_testapi/ui/pods/podsController.js122
-rw-r--r--testapi/opnfv_testapi/ui/profile/importPubKeyModal.html27
-rw-r--r--testapi/opnfv_testapi/ui/profile/profile.html44
-rw-r--r--testapi/opnfv_testapi/ui/profile/profileController.js219
-rw-r--r--testapi/opnfv_testapi/ui/profile/showPubKeyModal.html11
-rw-r--r--testapi/opnfv_testapi/ui/results-report/partials/editTestModal.html65
-rw-r--r--testapi/opnfv_testapi/ui/results-report/partials/fullTestListModal.html13
-rw-r--r--testapi/opnfv_testapi/ui/results-report/partials/reportDetails.html87
-rw-r--r--testapi/opnfv_testapi/ui/results-report/resultsReport.html185
-rw-r--r--testapi/opnfv_testapi/ui/results-report/resultsReportController.js869
-rw-r--r--testapi/opnfv_testapi/ui/results/results.html115
-rw-r--r--testapi/opnfv_testapi/ui/results/resultsController.js370
18 files changed, 2336 insertions, 0 deletions
diff --git a/testapi/opnfv_testapi/ui/about/about.html b/testapi/opnfv_testapi/ui/about/about.html
new file mode 100644
index 0000000..65860a8
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/auth-failure/authFailureController.js b/testapi/opnfv_testapi/ui/auth-failure/authFailureController.js
new file mode 100644
index 0000000..29d1d70
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/home/home.html b/testapi/opnfv_testapi/ui/home/home.html
new file mode 100644
index 0000000..47d747f
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/home/home.html
@@ -0,0 +1,23 @@
+<div class="jumbotron openstack-intro">
+ <div class="pull-right right openstack-intro__logo">
+ <img src="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/testapi/opnfv_testapi/ui/logout/logout.html b/testapi/opnfv_testapi/ui/logout/logout.html
new file mode 100644
index 0000000..38a5c36
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/logout/logout.html
@@ -0,0 +1 @@
+<div cg-busy="{promise:ctrl.redirectWait,message:'Logging you out...'}"></div>
diff --git a/testapi/opnfv_testapi/ui/logout/logoutController.js b/testapi/opnfv_testapi/ui/logout/logoutController.js
new file mode 100644
index 0000000..1b6d78c
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/pods/pods.html b/testapi/opnfv_testapi/ui/pods/pods.html
new file mode 100644
index 0000000..22f2934
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/pods/pods.html
@@ -0,0 +1,76 @@
+<h3>Pods</h3>
+<p>This page is used to create or query pods.<br>
+ Querying pods is open to everybody.<br>
+ But only login users are granted the privilege to create the new pod.
+</p>
+
+<div class="row" style="margin-bottom:24px;"></div>
+
+<div class="pod-create" ng-class="{ 'hidden': ! auth.isAuthenticated }">
+ <h4>Create</h4>
+ <div class="row">
+ <div ng-repeat="require in ctrl.createRequirements">
+ <div class="create-pod" style="margin-left:24px;">
+ <p class="input-group">
+ <label for="cpid">{{require.label|capitalize}}: </label>
+ <a ng-if="require.type == 'select'">
+ <select dynamic-model="'ctrl.' + require.label" ng-options="option for option in require.selects"></select>
+ </a>
+ <a ng-if="require.type == 'text'">
+ <input type="text" dynamic-model="'ctrl.' + require.label"/>
+ </a>
+ <a ng-if="require.type == 'textarea'">
+ <textarea rows="2" cols="50" dynamic-model="'ctrl.' + require.label">
+ </textarea>
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="col-md-3" style="margin-top:12px; margin-left:8px;">
+ <button type="submit" class="btn btn-primary" ng-click="ctrl.create()">Create</button>
+ </div>
+ </div>
+</div>
+
+<div class="pods-filters" style="margin-top:36px;">
+ <h4>Filters</h4>
+ <div class="row">
+ <div class="col-md-3" style="margin-top:12px; margin-left:8px;">
+ <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.podsRequest,message:'Loading'}"></div>
+
+<div ng-show="ctrl.data" class="pods-table" style="margin-top:24px; margin-left:8px;">
+ <table ng-data="ctrl.data.pods" ng-show="ctrl.data" class="table table-striped table-hover">
+ <tbody>
+ <tr ng-repeat-start="(index, pod) in ctrl.data.pods">
+ <td>
+ <a href="#" ng-click="showPod = !showPod">{{pod.name}}</a>
+ <div class="show-pod" ng-class="{ 'hidden': ! showPod }" style="margin-left:24px;">
+ <p>
+ owner: {{pod.owner}}<br>
+ role: {{pod.role}}<br>
+ mode: {{pod.mode}}<br>
+ create_date: {{pod.creation_date}}<br>
+ details: {{pod.details}}
+ </p>
+ </div>
+ </td>
+ </tr>
+ <tr ng-repeat-end=>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<br>
+<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/testapi/opnfv_testapi/ui/pods/podsController.js b/testapi/opnfv_testapi/ui/pods/podsController.js
new file mode 100644
index 0000000..489fa8a
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/pods/podsController.js
@@ -0,0 +1,122 @@
+/*
+ * 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('PodsController', PodsController);
+
+ PodsController.$inject = [
+ '$scope', '$http', '$filter', '$state', 'testapiApiUrl','raiseAlert'
+ ];
+
+ /**
+ * TestAPI Pods Controller
+ * This controller is for the '/pods' page where a user can browse
+ * through pods declared in TestAPI.
+ */
+ function PodsController($scope, $http, $filter, $state, testapiApiUrl,
+ raiseAlert) {
+ var ctrl = this;
+ ctrl.url = testapiApiUrl + '/pods';
+
+ ctrl.create = create;
+ ctrl.update = update;
+ ctrl.open = open;
+ ctrl.clearFilters = clearFilters;
+
+ ctrl.roles = ['community-ci', 'production-ci'];
+ ctrl.modes = ['metal', 'virtual'];
+ ctrl.createRequirements = [
+ {label: 'name', type: 'text', required: true},
+ {label: 'mode', type: 'select', selects: ctrl.modes},
+ {label: 'role', type: 'select', selects: ctrl.roles},
+ {label: 'details', type: 'textarea', required: false}
+ ];
+
+ ctrl.name = '';
+ ctrl.role = 'community-ci';
+ ctrl.mode = 'metal';
+ ctrl.details = '';
+
+ /**
+ * 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.update();
+ }
+
+ /**
+ * This will contact the TestAPI to create a new pod.
+ */
+ function create() {
+ ctrl.showError = false;
+
+ if(ctrl.name != ""){
+ var pods_url = ctrl.url;
+ var body = {
+ name: ctrl.name,
+ mode: ctrl.mode,
+ role: ctrl.role,
+ details: ctrl.details
+ };
+ ctrl.podsRequest =
+ $http.post(pods_url, body).error(function (data, status) {
+ ctrl.showError = true;
+ if(status == 403){
+ ctrl.error =
+ 'Error creating the new pod from server: Pod\'s name already exists'
+ }
+ });
+ }
+ else{
+ ctrl.showError = true;
+ ctrl.error = 'Name is missing.'
+ }
+ }
+
+ /**
+ * This will contact the TestAPI to get a listing of declared pods.
+ */
+ function update() {
+ ctrl.showError = false;
+ ctrl.podsRequest =
+ $http.get(ctrl.url).success(function (data) {
+ ctrl.data = data;
+ }).error(function (error) {
+ ctrl.data = null;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving pods from server: ' +
+ angular.toJson(error);
+ });
+ }
+ }
+})();
diff --git a/testapi/opnfv_testapi/ui/profile/importPubKeyModal.html b/testapi/opnfv_testapi/ui/profile/importPubKeyModal.html
new file mode 100644
index 0000000..0f55c27
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/profile/profile.html b/testapi/opnfv_testapi/ui/profile/profile.html
new file mode 100644
index 0000000..763f5d1
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/profile/profile.html
@@ -0,0 +1,44 @@
+<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</td> <td>{{auth.currentUser.user}}</td> </tr>
+ <tr> <td>Fullname</td> <td>{{auth.currentUser.fullname}}</td> </tr>
+ <tr> <td>Email</td> <td>{{auth.currentUser.email}}</td> </tr>
+ <tr> <td>Groups</td>
+ <td>
+ <div ng-repeat="group in auth.currentUser.groups">
+ {{group}}</br>
+ </div>
+ </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/testapi/opnfv_testapi/ui/profile/profileController.js b/testapi/opnfv_testapi/ui/profile/profileController.js
new file mode 100644
index 0000000..5dbdf7b
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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 + '/user/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/testapi/opnfv_testapi/ui/profile/showPubKeyModal.html b/testapi/opnfv_testapi/ui/profile/showPubKeyModal.html
new file mode 100644
index 0000000..5f63a5e
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/results-report/partials/editTestModal.html b/testapi/opnfv_testapi/ui/results-report/partials/editTestModal.html
new file mode 100644
index 0000000..583c9b9
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/results-report/partials/fullTestListModal.html b/testapi/opnfv_testapi/ui/results-report/partials/fullTestListModal.html
new file mode 100644
index 0000000..6db198b
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/results-report/partials/reportDetails.html b/testapi/opnfv_testapi/ui/results-report/partials/reportDetails.html
new file mode 100644
index 0000000..517e569
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/results-report/resultsReport.html b/testapi/opnfv_testapi/ui/results-report/resultsReport.html
new file mode 100644
index 0000000..5527121
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/results-report/resultsReportController.js b/testapi/opnfv_testapi/ui/results-report/resultsReportController.js
new file mode 100644
index 0000000..591ad40
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/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/testapi/opnfv_testapi/ui/results/results.html b/testapi/opnfv_testapi/ui/results/results.html
new file mode 100644
index 0000000..2ae5339
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/results/results.html
@@ -0,0 +1,115 @@
+<h3>{{ctrl.pageHeader}}</h3>
+<p>{{ctrl.pageParagraph}}</p>
+<form class="form-inline" ng-show="ctrl.isUserResults">
+<h4>Upload Results</h4>
+<div class="form-group col-m-3">
+ <input class="form-contrl btn btn-default" type = "file" file-model = "resultFile"/>
+</div>
+<div class="checkbox col-m-1">
+ <label>
+ <input type="checkbox" ng-model="ctrl.isPublic">public
+ </label>
+</div>
+<div class="form-group col-m-3">
+ <button class="btn btn-primary" ng-click = "ctrl.uploadFile()">upload result</button>
+</div>
+<div>
+<lable>{{ctrl.uploadState}}</label>
+</div>
+</form>
+<div class="row" style="margin-bottom:24px;"></div>
+<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-data="ctrl.data.result" ng-show="ctrl.data" class="table table-striped table-hover">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Pod</th>
+ <th>Project</th>
+ <th>Test Case</th>
+ <th>Installer</th>
+ <th>Version</th>
+ <th>Scenario</th>
+ <th>Criteria</th>
+ <th>Start Date</th>
+ <th>Stop Date</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr ng-repeat-start="(index, result) in ctrl.data.results">
+ <td>{{ result._id }}</td>
+ <td>{{ result.pod_name }}</td>
+ <td>{{ result.project_name }}</td>
+ <td>{{ result.case_name }}</td>
+ <td>{{ result.installer }}</td>
+ <td>{{ result.version }}</td>
+ <td>{{ result.scenario }}</td>
+ <td>{{ result.criteria }}</td>
+ <td>{{ result.start_date }}</td>
+ <td>{{ result.stop_date }}</td>
+ </tr>
+ <tr ng-repeat-end=>
+ </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/testapi/opnfv_testapi/ui/results/resultsController.js b/testapi/opnfv_testapi/ui/results/resultsController.js
new file mode 100644
index 0000000..cc6cc0b
--- /dev/null
+++ b/testapi/opnfv_testapi/ui/results/resultsController.js
@@ -0,0 +1,370 @@
+/*
+ * 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);
+
+ angular
+ .module('testapiApp')
+ .directive('fileModel', ['$parse', function ($parse) {
+ return {
+ restrict: 'A',
+ link: function(scope, element, attrs) {
+ var model = $parse(attrs.fileModel);
+ var modelSetter = model.assign;
+
+ element.bind('change', function(){
+ scope.$apply(function(){
+ modelSetter(scope, element[0].files[0]);
+ });
+ });
+ }
+ };
+ }]);
+
+ 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.uploadFile=uploadFile;
+ ctrl.update = update;
+ ctrl.open = open;
+ ctrl.clearFilters = clearFilters;
+ ctrl.associateMeta = associateMeta;
+ ctrl.getVersionList = getVersionList;
+ ctrl.getUserProducts = getUserProducts;
+ 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';
+ // need auth to browse
+ 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.';
+
+ ctrl.uploadState = '';
+
+ ctrl.isPublic = false;
+
+ if (ctrl.isUserResults) {
+ ctrl.authRequest = $scope.auth.doSignCheck()
+ .then(ctrl.update);
+ // ctrl.getUserProducts();
+ } else {
+ ctrl.update();
+ }
+
+
+ function uploadFileToUrl(file, uploadUrl){
+ var fd = new FormData();
+ fd.append('file', file);
+ fd.append('public', ctrl.isPublic)
+
+ $http.post(uploadUrl, fd, {
+ transformRequest: angular.identity,
+ headers: {'Content-Type': undefined}
+ })
+
+ .success(function(data){
+ var id = data.href.substr(data.href.lastIndexOf('/')+1);
+ ctrl.uploadState = "Upload succeed. Result id is " + id;
+ ctrl.update();
+ })
+
+ .error(function(data, status){
+ ctrl.uploadState = "Upload failed. Error code is " + status;
+ });
+ }
+
+ function uploadFile(){
+ var file = $scope.resultFile;
+ console.log('file is ' );
+ console.dir(file);
+
+ var uploadUrl = testapiApiUrl + "/results/upload";
+ uploadFileToUrl(file, uploadUrl);
+ };
+
+ /**
+ * 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 + '&from=' + start + ' 00:00:00';
+ }
+ var end = $filter('date')(ctrl.endDate, 'yyyy-MM-dd');
+ if (end) {
+ content_url = content_url + '&to=' + 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);
+ });
+ }
+
+ /**
+ * 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);
+ }
+
+ }
+})();