diff --git a/refstack-ui/app/app.js b/refstack-ui/app/app.js
index 44ad016b..6efa35ea 100644
--- a/refstack-ui/app/app.js
+++ b/refstack-ui/app/app.js
@@ -93,6 +93,26 @@
url: '/vendor/:vendorID',
templateUrl: '/components/vendors/vendor.html',
controller: 'VendorController as ctrl'
+ }).
+ state('userProducts', {
+ url: '/user_products',
+ templateUrl: '/components/products/products.html',
+ controller: 'ProductsController as ctrl'
+ }).
+ state('publicProducts', {
+ url: '/public_products',
+ templateUrl: '/components/products/products.html',
+ controller: 'ProductsController as ctrl'
+ }).
+ state('cloud', {
+ url: '/cloud/:id',
+ templateUrl: '/components/products/cloud.html',
+ controller: 'ProductController as ctrl'
+ }).
+ state('distro', {
+ url: '/distro/:id',
+ templateUrl: '/components/products/distro.html',
+ controller: 'ProductController as ctrl'
});
}
diff --git a/refstack-ui/app/components/products/cloud.html b/refstack-ui/app/components/products/cloud.html
new file mode 100644
index 00000000..eb25160e
--- /dev/null
+++ b/refstack-ui/app/components/products/cloud.html
@@ -0,0 +1,16 @@
+
Cloud Product
+
+
+
+
+ Name: {{ctrl.product.name}}
+ Product ID: {{ctrl.id}}
+ Description: {{ctrl.product.description}}
+ Publicity: {{ctrl.product.public ? 'Public' : 'Private'}}
+ Vendor Name: {{ctrl.vendor.name}}
+ Vendor Description: {{ctrl.vendor.description || '-'}}
+
+
+
+
+
diff --git a/refstack-ui/app/components/products/distro.html b/refstack-ui/app/components/products/distro.html
new file mode 100644
index 00000000..6638f358
--- /dev/null
+++ b/refstack-ui/app/components/products/distro.html
@@ -0,0 +1,16 @@
+Distro Product
+
+
+
+
+ Name: {{ctrl.product.name}}
+ Product ID: {{ctrl.id}}
+ Description: {{ctrl.product.description}}
+ Publicity: {{ctrl.product.public ? 'Public' : 'Private'}}
+ Vendor Name: {{ctrl.vendor.name}}
+ Vendor Description: {{ctrl.vendor.description || '-'}}
+
+
+
+
+
diff --git a/refstack-ui/app/components/products/partials/management.html b/refstack-ui/app/components/products/partials/management.html
new file mode 100644
index 00000000..f6ec4950
--- /dev/null
+++ b/refstack-ui/app/components/products/partials/management.html
@@ -0,0 +1,13 @@
+
diff --git a/refstack-ui/app/components/products/productController.js b/refstack-ui/app/components/products/productController.js
new file mode 100644
index 00000000..c2e60ef4
--- /dev/null
+++ b/refstack-ui/app/components/products/productController.js
@@ -0,0 +1,109 @@
+/*
+ * 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('refstackApp')
+ .controller('ProductController', ProductController);
+
+ ProductController.$inject = [
+ '$scope', '$http', '$state', '$stateParams', '$window',
+ 'refstackApiUrl', 'raiseAlert'
+ ];
+
+ /**
+ * RefStack Product Controller
+ * This controller is for the '/product/' details page where owner can
+ * view details of the product.
+ */
+ function ProductController($scope, $http, $state, $stateParams,
+ $window, refstackApiUrl, raiseAlert) {
+ var ctrl = this;
+
+ ctrl.getProduct = getProduct;
+ ctrl.deleteProduct = deleteProduct;
+ ctrl.switchProductPublicity = switchProductPublicity;
+
+ /** The product id extracted from the URL route. */
+ ctrl.id = $stateParams.id;
+
+ if (!$scope.auth.isAuthenticated) {
+ $state.go('home');
+ }
+
+ ctrl.getProduct();
+
+ /**
+ * This will contact the Refstack API to get a product information.
+ */
+ function getProduct() {
+ ctrl.showError = false;
+ ctrl.product = null;
+ // Construct the API URL based on user-specified filters.
+ var content_url = refstackApiUrl + '/products/' + ctrl.id;
+ ctrl.productRequest =
+ ctrl.productRequest = $http.get(content_url).success(
+ function(data) {
+ ctrl.product = data;
+ ctrl.product_properties =
+ angular.fromJson(data.properties);
+ }
+ ).error(function(error) {
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving from server: ' +
+ angular.toJson(error);
+ }).then(function() {
+ var url = refstackApiUrl + '/vendors/' +
+ ctrl.product.organization_id;
+ $http.get(url).success(function(data) {
+ ctrl.vendor = data;
+ }).error(function(error) {
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving from server: ' +
+ angular.toJson(error);
+ });
+ });
+ }
+
+ /**
+ * This will delete the product.
+ */
+ function deleteProduct() {
+ var url = [refstackApiUrl, '/products/', ctrl.id].join('');
+ $http.delete(url).success(function () {
+ $window.location.href = '/';
+ }).error(function (error) {
+ raiseAlert('danger', 'Error: ', error.detail);
+ });
+ }
+
+ /**
+ * This will switch public/private property of the product.
+ */
+ function switchProductPublicity() {
+ var url = [refstackApiUrl, '/products/', ctrl.id].join('');
+ $http.put(url, {public: !ctrl.product.public}).success(
+ function (data) {
+ ctrl.product = data;
+ ctrl.product_properties = angular.fromJson(data.properties);
+ }).error(function (error) {
+ raiseAlert('danger', 'Error: ', error.detail);
+ });
+ }
+ }
+})();
diff --git a/refstack-ui/app/components/products/products.html b/refstack-ui/app/components/products/products.html
new file mode 100644
index 00000000..679adcc2
--- /dev/null
+++ b/refstack-ui/app/components/products/products.html
@@ -0,0 +1,79 @@
+{{ctrl.pageHeader}}
+{{ctrl.pageParagraph}}
+
+
+
+
+
+
+
+ Name |
+ Product Type |
+ Description |
+ Vendor |
+ Visibility |
+
+
+
+
+
+ {{product.name}} |
+ {{product.name}} |
+ {{product.name}} |
+ {{ctrl.getProductTypeDescription(product.product_type)}} |
+ {{product.description}} |
+ {{ctrl.allVendors[product.organization_id].name}} |
+ {{product.public ? 'Public' : 'Private'}} |
+
+
+
+
+
+
+
+
Add new Product
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Error:
+ {{ctrl.error}}
+
diff --git a/refstack-ui/app/components/products/productsController.js b/refstack-ui/app/components/products/productsController.js
new file mode 100644
index 00000000..2338bdcd
--- /dev/null
+++ b/refstack-ui/app/components/products/productsController.js
@@ -0,0 +1,204 @@
+/*
+ * 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('refstackApp')
+ .controller('ProductsController', ProductsController);
+
+ ProductsController.$inject = [
+ '$rootScope', '$scope', '$http', '$state',
+ 'refstackApiUrl','raiseAlert'
+ ];
+
+ /**
+ * RefStack Products Controller
+ */
+ function ProductsController($rootScope, $scope, $http, $state,
+ refstackApiUrl, raiseAlert) {
+ var ctrl = this;
+
+ ctrl.update = update;
+ ctrl.updateData = updateData;
+ ctrl._filterProduct = _filterProduct;
+ ctrl.addProduct = addProduct;
+ ctrl.updateVendors = updateVendors;
+ ctrl.getProductTypeDescription = getProductTypeDescription;
+
+ /** Check to see if this page should display user-specific products. */
+ ctrl.isUserProducts = $state.current.name === 'userProducts';
+ /** Show private products in list for foundation admin */
+ ctrl.withPrivate = false;
+
+ /** Properties for adding new products */
+ ctrl.name = '';
+ ctrl.description = '';
+ ctrl.organizationId = '';
+
+ // Should only be on user-products-page if authenticated.
+ if (ctrl.isUserProducts && !$scope.auth.isAuthenticated) {
+ $state.go('home');
+ }
+
+ ctrl.pageHeader = ctrl.isUserProducts ?
+ 'My Products' : 'Public Products';
+
+ ctrl.pageParagraph = ctrl.isUserProducts ?
+ 'Your added products are listed here.' :
+ 'Public products are listed here.';
+
+ if (ctrl.isUserProducts) {
+ ctrl.authRequest = $scope.auth.doSignCheck()
+ .then(ctrl.updateVendors)
+ .then(ctrl.update);
+ } else {
+ ctrl.updateVendors();
+ ctrl.update();
+ }
+
+ ctrl.rawData = null;
+ ctrl.allVendors = {};
+ ctrl.isAdminView = $rootScope.auth
+ && $rootScope.auth.currentUser
+ && $rootScope.auth.currentUser.is_admin;
+
+ /**
+ * This will contact the Refstack API to get a listing of products.
+ */
+ function update() {
+ ctrl.showError = false;
+ // Construct the API URL based on user-specified filters.
+ var contentUrl = refstackApiUrl + '/products';
+ if (typeof ctrl.rawData == 'undefined'
+ || ctrl.rawData === null) {
+ ctrl.productsRequest =
+ $http.get(contentUrl).success(function (data) {
+ ctrl.rawData = data;
+ ctrl.updateData();
+ }).error(function (error) {
+ ctrl.rawData = null;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving Products listing from server: ' +
+ angular.toJson(error);
+ });
+ } else {
+ ctrl.updateData();
+ }
+ }
+
+ /**
+ * This will update data for view with current settings on page.
+ */
+ function updateData() {
+ ctrl.data = {};
+ ctrl.data.products = ctrl.rawData.products.filter(function(s) {
+ return ctrl._filterProduct(s); });
+ ctrl.data.products.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
+ }
+
+ /**
+ * Returns true if a specific product can be displayed on this page.
+ */
+ function _filterProduct(product) {
+ if (!ctrl.isUserProducts) {
+ return product.public;
+ }
+
+ if ($rootScope.auth.currentUser.is_admin) {
+ // TO-DO: filter out non-admin's items
+ // because public is not a correct flag for this
+ return product.public || ctrl.withPrivate;
+ }
+
+ return product.can_manage;
+ }
+
+ /**
+ * Get the product type description given the type integer.
+ */
+ function getProductTypeDescription(product_type) {
+ switch (product_type) {
+ case 0:
+ return 'Distro';
+ case 1:
+ return 'Public Cloud';
+ case 2:
+ return 'Hosted Private Cloud';
+ default:
+ return 'Unknown';
+ }
+ }
+
+ /**
+ * This will contact the Refstack API to get a listing of
+ * available vendors that can be used to associate with products.
+ */
+ function updateVendors() {
+ // Construct the API URL based on user-specified filters.
+ var contentUrl = refstackApiUrl + '/vendors';
+ ctrl.vendorsRequest =
+ $http.get(contentUrl).success(function (data) {
+ ctrl.vendors = Array();
+ ctrl.allVendors = {};
+ data.vendors.forEach(function(vendor) {
+ ctrl.allVendors[vendor.id] = vendor;
+ if (vendor.can_manage) {
+ ctrl.vendors.push(vendor);
+ }
+ });
+ ctrl.vendors.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
+ if (ctrl.vendors.length == 0) {
+ ctrl.vendors.push({name: 'Create New...', id: ''});
+ }
+ ctrl.organizationId = ctrl.vendors[0].id;
+ }).error(function (error) {
+ ctrl.vendors = null;
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error retrieving vendor listing from server: ' +
+ angular.toJson(error);
+ });
+ }
+
+ /**
+ * This will add new Product record.
+ */
+ function addProduct() {
+ var url = refstackApiUrl + '/products';
+ var data = {
+ name: ctrl.name,
+ description: ctrl.description,
+ organization_id: ctrl.organizationId,
+ product_type: parseInt(ctrl.productType)
+ };
+ ctrl.name = '';
+ ctrl.description = '';
+ $http.post(url, data).success(function (data) {
+ ctrl.rawData = null;
+ ctrl.update();
+ }).error(function (error) {
+ ctrl.showError = true;
+ ctrl.error =
+ 'Error adding new Product: ' + angular.toJson(error);
+ });
+ }
+ }
+})();
diff --git a/refstack-ui/app/components/vendors/vendorsController.js b/refstack-ui/app/components/vendors/vendorsController.js
index 6386d328..d597915c 100644
--- a/refstack-ui/app/components/vendors/vendorsController.js
+++ b/refstack-ui/app/components/vendors/vendorsController.js
@@ -74,8 +74,7 @@
&& $rootScope.auth.currentUser.is_admin;
/**
- * This will contact the Refstack API to get a listing of test run
- * results.
+ * This will contact the Refstack API to get a listing of vendors
*/
function update() {
ctrl.showError = false;
diff --git a/refstack-ui/app/index.html b/refstack-ui/app/index.html
index f3111645..426f2628 100644
--- a/refstack-ui/app/index.html
+++ b/refstack-ui/app/index.html
@@ -47,6 +47,8 @@
+
+
diff --git a/refstack-ui/app/shared/header/header.html b/refstack-ui/app/shared/header/header.html
index c50a4399..aa6984fd 100644
--- a/refstack-ui/app/shared/header/header.html
+++ b/refstack-ui/app/shared/header/header.html
@@ -25,6 +25,7 @@ RefStack
@@ -36,6 +37,7 @@ RefStack
Profile
diff --git a/refstack-ui/app/shared/header/headerController.js b/refstack-ui/app/shared/header/headerController.js
index ac1d6104..05727d2e 100644
--- a/refstack-ui/app/shared/header/headerController.js
+++ b/refstack-ui/app/shared/header/headerController.js
@@ -56,7 +56,8 @@
* public or user one.
*/
function isCatalogActive(type) {
- return ctrl.isActive('/' + type + '_vendors');
+ return ctrl.isActive('/' + type + '_vendors')
+ || ctrl.isActive('/' + type + '_products');
}
}
})();
diff --git a/refstack-ui/tests/unit/ControllerSpec.js b/refstack-ui/tests/unit/ControllerSpec.js
index c078ec02..6d28b7ba 100644
--- a/refstack-ui/tests/unit/ControllerSpec.js
+++ b/refstack-ui/tests/unit/ControllerSpec.js
@@ -947,4 +947,166 @@ describe('Refstack controllers', function () {
$httpBackend.flush();
});
});
+
+ describe('ProductsController', function() {
+ var rootScope, scope, ctrl;
+ var vendResp = {'vendors': [{'can_manage': true,
+ 'type': 3,
+ 'name': 'Foo',
+ 'id': '123'}]};
+ var prodResp = {'products': [{'id': 'abc',
+ 'product_type': 1,
+ 'public': 1,
+ 'name': 'Foo Product',
+ 'organization_id': '123'}]};
+
+ beforeEach(inject(function ($controller, $rootScope) {
+ scope = $rootScope.$new();
+ rootScope = $rootScope.$new();
+ rootScope.auth = {'currentUser' : {'is_admin': false,
+ 'openid': 'foo'}
+ };
+ ctrl = $controller('ProductsController',
+ {$rootScope: rootScope, $scope: scope}
+ );
+ $httpBackend.when('GET', fakeApiUrl +
+ '/vendors').respond(vendResp);
+ $httpBackend.when('GET', fakeApiUrl +
+ '/products').respond(prodResp);
+ }));
+
+ it('should have a function to get/update vendors',
+ function () {
+ $httpBackend.flush();
+ var newVendResp = {'vendors': [{'name': 'Foo',
+ 'id': '123',
+ 'can_manage': true},
+ {'name': 'Bar',
+ 'id': '345',
+ 'can_manage': false}]};
+ $httpBackend.expectGET(fakeApiUrl + '/vendors')
+ .respond(200, newVendResp);
+ ctrl.updateVendors();
+ $httpBackend.flush();
+ expect(ctrl.allVendors).toEqual({'123': {'name': 'Foo',
+ 'id': '123',
+ 'can_manage': true},
+ '345': {'name': 'Bar',
+ 'id': '345',
+ 'can_manage': false}});
+ expect(ctrl.vendors).toEqual([{'name': 'Foo',
+ 'id': '123',
+ 'can_manage': true}]);
+ });
+
+ it('should have a function to get products',
+ function () {
+ $httpBackend.expectGET(fakeApiUrl + '/products')
+ .respond(200, prodResp);
+ ctrl.update();
+ $httpBackend.flush();
+ expect(ctrl.rawData).toEqual(prodResp);
+ });
+
+ it('should have a function to update the view',
+ function () {
+ $httpBackend.flush();
+ ctrl.allVendors = {'123': {'name': 'Foo',
+ 'id': '123',
+ 'can_manage': true}};
+ ctrl.updateData();
+ var expectedData = {'products': [{'id': 'abc',
+ 'product_type': 1,
+ 'public': 1,
+ 'name': 'Foo Product',
+ 'organization_id': '123'}]};
+ expect(ctrl.data).toEqual(expectedData);
+ });
+
+ it('should have a function to map product types with descriptions',
+ function () {
+ expect(ctrl.getProductTypeDescription(0)).toEqual('Distro');
+ expect(ctrl.getProductTypeDescription(1))
+ .toEqual('Public Cloud');
+ expect(ctrl.getProductTypeDescription(2))
+ .toEqual('Hosted Private Cloud');
+ expect(ctrl.getProductTypeDescription(5)).toEqual('Unknown');
+ });
+ });
+
+ describe('ProductController', function() {
+ var rootScope, scope, stateParams, ctrl;
+ var fakeProdResp = {'product_type': 1,
+ 'product_id': null,
+ 'name': 'Good Stuff',
+ 'created_at': '2016-01-01 01:02:03',
+ 'updated_at': '2016-06-15 01:02:04',
+ 'properties': null,
+ 'organization_id': 'fake-org-id',
+ 'public': true,
+ 'can_manage': true,
+ 'created_by_user': 'fake-open-id',
+ 'type': 0,
+ 'id': '1234',
+ 'description': 'some description'};
+ var fakeVendorResp = {'id': 'fake-org-id',
+ 'type': 3,
+ 'can_manage': true,
+ 'properties' : {},
+ 'name': 'Foo Vendor',
+ 'description': 'foo bar'};
+ var fakeWindow = {
+ location: {
+ href: ''
+ }
+ };
+
+ beforeEach(inject(function ($controller, $rootScope) {
+ scope = $rootScope.$new();
+ rootScope = $rootScope.$new();
+ stateParams = {id: 1234};
+ rootScope.auth = {'currentUser' : {'is_admin': false,
+ 'openid': 'foo'}
+ };
+ ctrl = $controller('ProductController',
+ {$rootScope: rootScope, $scope: scope,
+ $stateParams: stateParams, $window: fakeWindow}
+ );
+ $httpBackend.when('GET', fakeApiUrl +
+ '/products/1234').respond(fakeProdResp);
+ $httpBackend.when('GET', fakeApiUrl +
+ '/vendors/fake-org-id').respond(fakeVendorResp);
+ }));
+
+ it('should have a function to get product information',
+ function () {
+ $httpBackend.expectGET(fakeApiUrl + '/products/1234')
+ .respond(200, fakeProdResp);
+ $httpBackend.expectGET(fakeApiUrl + '/vendors/fake-org-id')
+ .respond(200, fakeVendorResp);
+ ctrl.getProduct();
+ $httpBackend.flush();
+ expect(ctrl.product).toEqual(fakeProdResp);
+ expect(ctrl.vendor).toEqual(fakeVendorResp);
+ });
+
+ it('should have a function to delete a product',
+ function () {
+ $httpBackend.expectDELETE(fakeApiUrl + '/products/1234')
+ .respond(202, '');
+ ctrl.deleteProduct();
+ $httpBackend.flush();
+ expect(fakeWindow.location.href).toEqual('/');
+ });
+
+ it('should have a function to switch the publicity of a project',
+ function () {
+ ctrl.product = {'public': true};
+ $httpBackend.expectPUT(fakeApiUrl + '/products/1234',
+ {'public': false})
+ .respond(200, fakeProdResp);
+ ctrl.switchProductPublicity();
+ $httpBackend.flush();
+ });
+ });
});