diff options
author | SerenaFeng <feng.xiaowei@zte.com.cn> | 2017-05-12 01:49:57 +0800 |
---|---|---|
committer | SerenaFeng <feng.xiaowei@zte.com.cn> | 2017-05-12 10:11:57 +0800 |
commit | f562c31e824f573d9a3254a1eacb4981b29290eb (patch) | |
tree | fd5526fc049fae9760da27b64318ad3fe5ce5767 /utils/test/testapi/3rd_party/static/testapi-ui/components | |
parent | a16b903c9765049bd28102c812b8307090a97e16 (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')
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™ 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()">×</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™ 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> + + <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()">×</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"> — + <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"> — + <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); + } + + } +})(); |