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 @@ +
+ + Delete +
+ + Make Product PrivatePublic +
+
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}}

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
NameProduct TypeDescriptionVendorVisibility
{{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

+
+
+ +

+ +

+
+
+ +

+ +

+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ + 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(); + }); + }); });