Add products UI base

This will add the user interface for managing/viewing products
for vendors.

Change-Id: I79859fe0a661dc8633786cf90b4b964ca0e99d64
Co-Authored-By: Andrey Pavlov <andrey-mp@yandex.ru>
This commit is contained in:
Paul Van Eck 2016-06-14 18:13:06 -07:00
parent 3c82bc3443
commit e6026d2251
12 changed files with 626 additions and 3 deletions

View File

@ -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'
});
}

View File

@ -0,0 +1,16 @@
<h3>Cloud Product</h3>
<div ng-show="ctrl.product" class="container-fluid">
<div class="row">
<div class="pull-left">
<div class="test-report">
<strong>Name:</strong> {{ctrl.product.name}}<br />
<strong>Product ID:</strong> {{ctrl.id}}<br />
<strong>Description:</strong> {{ctrl.product.description}}<br />
<strong>Publicity:</strong> {{ctrl.product.public ? 'Public' : 'Private'}}<br />
<strong>Vendor Name:</strong> {{ctrl.vendor.name}}<br />
<strong>Vendor Description:</strong> {{ctrl.vendor.description || '-'}}<br />
</div>
</div>
<div ng-include src="'components/products/partials/management.html'"></div>
</div>
</div>

View File

@ -0,0 +1,16 @@
<h3>Distro Product</h3>
<div ng-show="ctrl.product" class="container-fluid">
<div class="row">
<div class="pull-left">
<div class="test-report">
<strong>Name:</strong> {{ctrl.product.name}}<br />
<strong>Product ID:</strong> {{ctrl.id}}<br />
<strong>Description:</strong> {{ctrl.product.description}}<br />
<strong>Publicity:</strong> {{ctrl.product.public ? 'Public' : 'Private'}}<br />
<strong>Vendor Name:</strong> {{ctrl.vendor.name}}<br />
<strong>Vendor Description:</strong> {{ctrl.vendor.description || '-'}}<br />
</div>
</div>
<div ng-include src="'components/products/partials/management.html'"></div>
</div>
</div>

View File

@ -0,0 +1,13 @@
<div class="pull-right">
<a ng-if="ctrl.product.can_manage"
href="javascript:void(0)"
ng-click="ctrl.deleteProduct()"
confirm="Are you sure you want to delete {{ctrl.product.name}}?">
Delete
</a><br />
<a ng-if="ctrl.product.can_manage && (ctrl.vendor.type == 0 || ctrl.vendor.type == 3)"
href="javascript:void(0)" ng-click="ctrl.switchProductPublicity()"
confirm="Are you sure you want to switch publicity of this product?">
Make Product <span ng-if="ctrl.product.public">Private</span><span ng-if="!ctrl.product.public">Public</span>
</a><br />
</div>

View File

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

View File

@ -0,0 +1,79 @@
<h3>{{ctrl.pageHeader}}</h3>
<p>{{ctrl.pageParagraph}}</p>
<div ng-show="ctrl.data" class="products-table">
<label ng-if="ctrl.isAdminView && ctrl.isUserProducts">
<input type="checkbox" ng-model="ctrl.withPrivate" ng-change="ctrl.updateData();">&nbsp;Show private
</label>
<br />
<table ng-show="ctrl.data" class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Product Type</th>
<th>Description</th>
<th>Vendor</th>
<th>Visibility</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="product in ctrl.data.products">
<td ng-if="ctrl.isUserProducts && product.product_type == 0"><a ui-sref="distro({id: product.id})">{{product.name}}</a></td>
<td ng-if="ctrl.isUserProducts && product.product_type != 0"><a ui-sref="cloud({id: product.id})">{{product.name}}</a></td>
<td ng-if="!ctrl.isUserProducts">{{product.name}}</td>
<td>{{ctrl.getProductTypeDescription(product.product_type)}}</td>
<td>{{product.description}}</td>
<td>{{ctrl.allVendors[product.organization_id].name}}</td>
<td>{{product.public ? 'Public' : 'Private'}}</td>
</tr>
</tbody>
</table>
</div>
<div ng-if="ctrl.isUserProducts">
<hr />
<h4>Add new Product</h4>
<div class="row">
<div class="col-md-2">
<label>Name</label>
<p class="input-group">
<input type="text" class="form-control" ng-model="ctrl.name" />
</p>
</div>
<div class="col-md-4">
<label>Description</label>
<p class="input-group">
<input type="text" class="form-control" size="80"
ng-model="ctrl.description" />
</p>
</div>
<div class="col-md-2">
<label>Product type:</label>
<select ng-model="ctrl.productType" class="form-control">
<option value="{{0}}">{{ctrl.getProductTypeDescription(0)}}</option>
<option value="{{1}}">{{ctrl.getProductTypeDescription(1)}}</option>
<option value="{{2}}">{{ctrl.getProductTypeDescription(2)}}</option>
</select>
</div>
<div class="col-md-2">
<label>Vendor:</label>
<select ng-model="ctrl.organizationId" class="form-control">
<option ng-repeat="vendor in ctrl.vendors" value="{{vendor.id}}">{{vendor.name}}</option>
</select>
</div>
<div class="col-md-2" style="margin-top:24px;">
<button type="submit" class="btn btn-primary" ng-click="ctrl.addProduct()">Add Product</button>
</div>
</div>
</div>
<div cg-busy="{promise:ctrl.authRequest,message:'Loading'}"></div>
<div cg-busy="{promise:ctrl.productsRequest,message:'Loading'}"></div>
<div cg-busy="{promise:ctrl.vendorsRequest,message:'Loading'}"></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>

View File

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

View File

@ -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;

View File

@ -47,6 +47,8 @@
<script src="components/logout/logoutController.js"></script>
<script src="components/vendors/vendorController.js"></script>
<script src="components/vendors/vendorsController.js"></script>
<script src="components/products/productController.js"></script>
<script src="components/products/productsController.js"></script>
<!-- Filters -->
<script src="shared/filters.js"></script>

View File

@ -25,6 +25,7 @@ RefStack
</a>
<ul class="dropdown-menu">
<li><a ui-sref="publicVendors">Vendors</a></li>
<li><a ui-sref="publicProducts">Products</a></li>
</ul>
</li>
</ul>
@ -36,6 +37,7 @@ RefStack
</a>
<ul class="dropdown-menu">
<li><a ui-sref="userVendors">My Vendors</a></li>
<li><a ui-sref="userProducts">My Products</a></li>
</ul>
</li>
<li ng-class="{ active: header.isActive('/profile')}" ng-if="auth.isAuthenticated"><a ui-sref="profile">Profile</a></li>

View File

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

View File

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