From 32283d03cc312df8048e93457296d08516121fe8 Mon Sep 17 00:00:00 2001 From: Michael Krotscheck Date: Thu, 16 Jan 2014 19:12:54 -0800 Subject: [PATCH] Storyboard API Interface and basic project management Here I add three major components: Firstly, the API abstractions (resources) that drive our consumption of the storyboard API. Secondly, a series of API mock interceptors that trigger when HTTP requests are made and simulate the existence of the storyboard API. Lastly, a basic UI for project creation, listing, and management. Change-Id: Idbce8252237b0f9fbb9dd2330b952f9a6432c694 --- .jshintrc | 4 +- bower.json | 16 +- .../controllers/project_detail_controller.js | 142 +++++++++++++++ .../controllers/project_list_controller.js | 42 ++++- .../controllers/project_new_controller.js | 54 ++++++ src/app/projects/module.js | 24 ++- .../services/http/http_error_broadcaster.js | 3 +- .../{resource => mock}/auth_provider_mock.js | 57 +++--- src/app/services/mock/mock_service.js | 163 ++++++++++++++++++ src/app/services/mock/project_mock.js | 58 +++++++ src/app/services/mock/task_mock.js | 34 ++++ src/app/services/mock/team.js | 34 ++++ src/app/services/mock/user_mock.js | 34 ++++ src/app/services/module.js | 3 +- .../provider/storyboard_api_signature.js | 54 ++++++ src/app/services/resource/auth_provider.js | 29 +--- src/app/services/resource/project.js | 31 ++++ src/app/services/resource/project_group.js | 50 ++++++ src/app/services/resource/task.js | 30 ++++ src/app/services/resource/team.js | 31 ++++ src/app/services/resource/user.js | 31 ++++ src/app/templates/auth/provider/login.html | 4 + src/app/templates/project/detail.html | 38 ++++ src/app/templates/project/edit.html | 84 +++++++++ src/app/templates/project/list.html | 79 ++++++--- src/app/templates/project/new.html | 73 ++++++++ src/styles/bootstrap_addons.less | 24 +++ src/styles/main.less | 1 + test/functional/auth/auth_route.js | 1 - .../http/http_error_broadcaster_test.js | 86 +++++++++ .../provider/storyboard_api_base_test.js | 59 +++++++ .../provider/storyboard_api_signature_test.js | 93 ++++++++++ 32 files changed, 1371 insertions(+), 95 deletions(-) create mode 100644 src/app/projects/controllers/project_detail_controller.js create mode 100644 src/app/projects/controllers/project_new_controller.js rename src/app/services/{resource => mock}/auth_provider_mock.js (52%) create mode 100644 src/app/services/mock/mock_service.js create mode 100644 src/app/services/mock/project_mock.js create mode 100644 src/app/services/mock/task_mock.js create mode 100644 src/app/services/mock/team.js create mode 100644 src/app/services/mock/user_mock.js create mode 100644 src/app/services/provider/storyboard_api_signature.js create mode 100644 src/app/services/resource/project.js create mode 100644 src/app/services/resource/project_group.js create mode 100644 src/app/services/resource/task.js create mode 100644 src/app/services/resource/team.js create mode 100644 src/app/services/resource/user.js create mode 100644 src/app/templates/project/detail.html create mode 100644 src/app/templates/project/edit.html create mode 100644 src/app/templates/project/new.html create mode 100644 src/styles/bootstrap_addons.less create mode 100644 test/unit/services/http/http_error_broadcaster_test.js create mode 100644 test/unit/services/provider/storyboard_api_base_test.js create mode 100644 test/unit/services/provider/storyboard_api_signature_test.js diff --git a/.jshintrc b/.jshintrc index a751de03..6285cf06 100644 --- a/.jshintrc +++ b/.jshintrc @@ -24,7 +24,7 @@ "browser": true, "esnext": true, "bitwise": true, - "camelcase": true, + "camelcase": false, "curly": true, "eqeqeq": true, "immed": true, @@ -56,6 +56,8 @@ "inject": false, "it": false, "spyOn": false, + "runs": false, + "waitsFor": false, // functional test constants "browser": false, diff --git a/bower.json b/bower.json index efdb42a3..dd619b28 100644 --- a/bower.json +++ b/bower.json @@ -4,19 +4,19 @@ "dependencies": { "jquery": "2.0.3", "font-awesome": "4.0", - "angular": "1.2.5", - "angular-resource": "1.2.5", - "angular-cookies": "1.2.5", - "angular-sanitize": "1.2.5", + "angular": "1.2.9", + "angular-resource": "1.2.9", + "angular-cookies": "1.2.9", + "angular-sanitize": "1.2.9", "bootstrap": "3.0.0", - "angular-ui-router": "0.2.0", + "angular-ui-router": "0.2.7", "angular-translate": "1.1.1" }, "devDependencies": { - "angular-mocks": "1.2.5", - "angular-scenario": "1.2.5" + "angular-mocks": "1.2.9", + "angular-scenario": "1.2.9" }, "resolutions": { - "angular": "1.2.5" + "angular": "1.2.9" } } diff --git a/src/app/projects/controllers/project_detail_controller.js b/src/app/projects/controllers/project_detail_controller.js new file mode 100644 index 00000000..ac5ae718 --- /dev/null +++ b/src/app/projects/controllers/project_detail_controller.js @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2013 Hewlett-Packard Development Company, L.P. + * + * 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. + */ +/** + * Project detail & manipulation controller. Usable for any view that wants to + * view, edit, or delete a project, though views don't have to use all the + * functions therein. Includes flags for busy time, error responses and more. + * + * This controller assumes that the $stateParams object is both injectable and + * contains an ":id" property that indicates which project should be loaded. At + * the moment it will only set a 'isLoading' flag to indicate that data is + * loading. If loading the data is anticipated to take longer than 3 seconds, + * this will need to be updated to display a sane progress. + * + * Do not allow loading of this (or any) controller to take longer than 10 + * seconds. 3 is preferable. + */ +angular.module('sb.projects').controller('ProjectDetailController', + function ($scope, $state, $stateParams, Project) { + 'use strict'; + + // Parse the ID + var id = $stateParams.hasOwnProperty('id') ? + parseInt($stateParams.id, 10) : + null; + + /** + * The project we're manipulating right now. + * + * @type Project + */ + $scope.project = {}; + + /** + * UI flag for when we're initially loading the view. + * + * @type {boolean} + */ + $scope.isLoading = true; + + /** + * UI view for when a change is round-tripping to the server. + * + * @type {boolean} + */ + $scope.isUpdating = false; + + /** + * Any error objects returned from the services. + * + * @type {{}} + */ + $scope.error = {}; + + /** + * Generic service error handler. Assigns errors to the view's scope, + * and unsets our flags. + */ + function handleServiceError(error) { + // We've encountered an error. + $scope.error = error; + $scope.isLoading = false; + $scope.isUpdating = false; + } + + + // Sanity check, do we actually have an ID? (zero is falsy) + if (!id && id !== 0) { + // We should never reach this, however that logic lives outside + // of this controller which could be unknowningly refactored. + $scope.error = { + error: true, + error_code: 404, + error_message: 'You did not provide a valid ID.' + }; + $scope.isLoading = false; + } else { + // We've got an ID, so let's load it... + Project.read( + {'id': id}, + function (result) { + // We've got a result, assign it to the view and unset our + // loading flag. + $scope.project = result; + $scope.isLoading = false; + }, + handleServiceError + ); + } + + /** + * Scope method, invoke this when you want to update the project. + */ + $scope.update = function () { + // Set our progress flags and clear previous error conditions. + $scope.isUpdating = true; + $scope.error = {}; + + // Invoke the save method and wait for results. + $scope.project.$update( + function () { + // Unset our loading flag and navigate to the detail view. + $scope.isUpdating = false; + $state.go('project.detail', {id: $scope.project.id}); + }, + handleServiceError + ); + }; + + + /** + * Scope method, invoke this when you'd like to delete this project. + */ + $scope.remove = function () { + // Set our progress flags and clear previous error conditions. + $scope.isUpdating = true; + $scope.error = {}; + + // Try to delete. + $scope.project.$delete( + function () { + // The deletion was successful, so head back to the list + // view. + $scope.isUpdating = false; + $state.go('project.list'); + }, + handleServiceError + ); + }; + }); diff --git a/src/app/projects/controllers/project_list_controller.js b/src/app/projects/controllers/project_list_controller.js index 1ce591ed..6135dc93 100644 --- a/src/app/projects/controllers/project_list_controller.js +++ b/src/app/projects/controllers/project_list_controller.js @@ -15,14 +15,48 @@ */ /** - * The Storyboard project submodule handles most activity surrounding the - * creation and management of projects. + * The project list controller handles discovery for all projects, including + * search. Note that it is assumed that we implemented a search (inclusive), + * rather than a browse (exclusive) approach. */ angular.module('sb.projects').controller('ProjectListController', - function ($scope) { + function ($scope, Project) { 'use strict'; - $scope.search = function () { + // Variables and methods available to the template... + $scope.projects = []; + $scope.searchQuery = ''; + $scope.isSearching = false; + /** + * The search method. + */ + $scope.search = function () { + // Clear the scope and set the progress flag. + $scope.error = {}; + $scope.isSearching = true; + $scope.projects = []; + + // Execute the project query. + Project.search( + // Enable this once the API's there, mocks don't support + // searches yet + {/* q: $scope.searchQuery || '' */}, + function (result) { + // Successful search results, apply the results to the + // scope and unset our progress flag. + $scope.projects = result; + $scope.isSearching = false; + }, + function (error) { + // Error search results, show the error in the UI and + // unset our progress flag. + $scope.error = error; + $scope.isSearching = false; + } + ); }; + + // Initialize the view with a default search. + $scope.search(); }); diff --git a/src/app/projects/controllers/project_new_controller.js b/src/app/projects/controllers/project_new_controller.js new file mode 100644 index 00000000..28c6ace2 --- /dev/null +++ b/src/app/projects/controllers/project_new_controller.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * View controller for the new project form. Includes an intermediary 'saving' + * flag as well as room for an error response (though until we get a real API + * that'll be a bit tricky to test). + */ +angular.module('sb.projects').controller('ProjectNewController', + function ($scope, $state, Project) { + 'use strict'; + + // View parameters. + $scope.newProject = new Project(); + $scope.isCreating = false; + $scope.error = {}; + + /** + * Submits the newly created project. If an error response is received, + * assigns it to the view and unsets various flags. The template + * should know how to handle it. + */ + $scope.createProject = function () { + + // Clear everything and set the progress flag... + $scope.isCreating = true; + $scope.error = {}; + + $scope.newProject.$create( + function () { + // Success! + $state.go('project.list'); + }, + function (error) { + // Error received. Ho hum. + $scope.isCreating = false; + $scope.error = error; + } + ); + }; + }); diff --git a/src/app/projects/module.js b/src/app/projects/module.js index ebd4f681..b535dd90 100644 --- a/src/app/projects/module.js +++ b/src/app/projects/module.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. * * 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 @@ -17,10 +17,8 @@ /** * The Storyboard project submodule handles most activity surrounding the * creation and management of projects. - * - * @author Michael Krotscheck */ -angular.module('sb.projects', ['ui.router', 'sb.services']) +angular.module('sb.projects', ['ui.router', 'sb.services', 'sb.util']) .config(function ($stateProvider, $urlRouterProvider) { 'use strict'; @@ -36,6 +34,22 @@ angular.module('sb.projects', ['ui.router', 'sb.services']) }) .state('project.list', { url: '/list', - templateUrl: 'app/templates/project/provider.html' + templateUrl: 'app/templates/project/list.html', + controller: 'ProjectListController' + }) + .state('project.edit', { + url: '/{id:[0-9]+}/edit', + templateUrl: 'app/templates/project/edit.html', + controller: 'ProjectDetailController' + }) + .state('project.detail', { + url: '/{id:[0-9]+}', + templateUrl: 'app/templates/project/detail.html', + controller: 'ProjectDetailController' + }) + .state('project.new', { + url: '/new', + templateUrl: 'app/templates/project/new.html', + controller: 'ProjectNewController' }); }); diff --git a/src/app/services/http/http_error_broadcaster.js b/src/app/services/http/http_error_broadcaster.js index eb48628b..f24bc7ae 100644 --- a/src/app/services/http/http_error_broadcaster.js +++ b/src/app/services/http/http_error_broadcaster.js @@ -51,8 +51,9 @@ angular.module('sb.services') * Handle a fail response. */ responseError: function (response) { + if (!!response) { - sendEvent(response.status, response.body); + sendEvent(response.status, response.data); } return $q.reject(response); diff --git a/src/app/services/resource/auth_provider_mock.js b/src/app/services/mock/auth_provider_mock.js similarity index 52% rename from src/app/services/resource/auth_provider_mock.js rename to src/app/services/mock/auth_provider_mock.js index 82683ca7..b96c609a 100644 --- a/src/app/services/resource/auth_provider_mock.js +++ b/src/app/services/mock/auth_provider_mock.js @@ -16,30 +16,44 @@ /** * Mock resource responses for the AuthProvider resource. - * - * @author Michael Krotscheck */ angular.module('sb.services') - .run(function ($httpBackend, $injector) { + .run(function (mock) { 'use strict'; - $httpBackend = $injector.get('$httpBackend'); var authProviders = [ { - 'id': 1, + 'id': 0, 'type': 'openid', 'title': 'OpenID', - 'url': 'https://www.google.com/prediscovered' + - '/redirection/url', + 'url': 'https://login.launchpad.net/+openid', 'params': { - 'list': 'of', - 'additional': 'parameters', - 'required': 'for.this.provider' + 'openid.ns': 'http://specs.openid.net/auth/2.0', + 'openid.claimed_id': 'http://specs.openid.net/auth' + + '/2.0/identifier_select', + 'openid.identity': 'http://specs.openid.net/auth' + + '/2.0/identifier_select', +// 'openid.return_to': 'https://review.openstack.org/ +// openid?gerrit.mode=SIGN_IN&gerrit.token=%2Fq%2Fstatus%3Aopen%2Cn%2Cz', +// 'openid.realm': 'https://review.openstack.org/', + 'openid.assoc_handle': '{HMAC-SHA256}{52c79079}{z+v4vA==}', + 'openid.mode': 'checkid_setup', + 'openid.ns.sreg': 'http://openid.net/sreg/1.0', + 'openid.sreg.required': 'fullname,email', + 'openid.ns.ext2': 'http://openid.net/srv/ax/1.0', + 'openid.ext2.mode': 'fetch_request', + 'openid.ext2.type.FirstName': 'http://schema.openid.net/' + + 'namePerson/first', + 'openid.ext2.type.LastName': 'http://schema.openid.net/' + + 'namePerson/last', + 'openid.ext2.type.Email': 'http://schema.openid.net/' + + 'contact/email', + 'openid.ext2.required': 'FirstName,LastName,Email' } }, { - 'id': 2, + 'id': 1, 'type': 'openid_connect', 'title': 'OpenID Connect', 'url': 'https://www.google.com/prediscovered' + @@ -51,7 +65,7 @@ angular.module('sb.services') } }, { - 'id': 3, + 'id': 2, 'type': 'ldap', 'title': 'LDAP', 'url': 'https://www.google.com/prediscovered' + @@ -64,20 +78,9 @@ angular.module('sb.services') } ]; - $httpBackend.when('GET', '/api/v1/auth/provider') - .respond( - { - total: 1, - offset: 0, - limit: 10, - results: authProviders - } - ); + mock.api('/api/v1/auth/provider', + '/api/v1/auth/provider/:id', + 'id', + authProviders); - $httpBackend.when('GET', '/api/v1/auth/provider/1') - .respond(authProviders[0]); - $httpBackend.when('GET', '/api/v1/auth/provider/2') - .respond(authProviders[1]); - $httpBackend.when('GET', '/api/v1/auth/provider/3') - .respond(authProviders[2]); }); \ No newline at end of file diff --git a/src/app/services/mock/mock_service.js b/src/app/services/mock/mock_service.js new file mode 100644 index 00000000..cc534ff2 --- /dev/null +++ b/src/app/services/mock/mock_service.js @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * This service creates an automatic CRUD mock based on provided + * urls and data sets. + * + * TODO(krotscheck): Once we have a real API, we can remove this. + */ + +angular.module('sb.services') + .factory('mock', function ($log, $urlMatcherFactory, $httpBackend) { + 'use strict'; + + /** + * URL matcher factory generator, used for testing API urls for + * mocking. + * + * @param testUrl + */ + function matchUrl(testUrl) { + var urlMatcher = $urlMatcherFactory.compile(testUrl); + + return { + test: function (url) { + return urlMatcher.exec(url); + } + }; + } + + /** + * Utility method that extracts the array index from the default data + * passed. Necessary because to simulate our mock list, we're splicing + * all the time, so the actual indexes of the array may not match the + * ID's of the items therein. + */ + function getIndexById(defaultData, idParamName, id) { + for (var i = 0; i < defaultData.length; i++) { + var item = defaultData[i]; + if (item[idParamName] === id) { + return i; + } + } + + return false; + } + + return { + /** + * This method mocks an entire RESTful api endpoint. + */ + api: function (searchUrl, crudUrl, crudIdParamName, defaultData) { + this.search(searchUrl, defaultData); + this.crud(searchUrl, crudUrl, crudIdParamName, defaultData); + }, + + /** + * This method creates a mock search service for the passed URL and + * the provided data hash. + */ + search: function (searchUrl, defaultData) { + $httpBackend.when('GET', matchUrl(searchUrl)) + .respond(function (method, url) { + $log.info('[mock] ' + method + ' ' + url); + return [200, { + total: defaultData.length, + offset: 0, + limit: defaultData.length, + results: defaultData + }]; + } + ); + }, + /** + * This method creates a mock CRUD service for the passed URL, + * ID parameter name, and data hash + */ + crud: function (createUrl, crudUrl, idParamName, defaultData) { + var crudMatcher = matchUrl(crudUrl); + var createMatcher = matchUrl(createUrl); + + /** + * Mock responder for a POST action. Extracts the ID from the + * last item in our default data array and increments it, then + * adds another item with that same ID. + */ + var createResolver = function (method, url, body) { + $log.info('[mock] ' + method + ' ' + url); + + body = JSON.parse(body); + var now = Math.round(new Date().getTime() / 1000); + body.id = defaultData[defaultData.length - 1].id + 1; + // jshint -W106 + body.created_at = now; + body.updated_at = now; + // jshint +W106 + defaultData[body.id] = body; + console.warn(defaultData); + return [201, body]; + }; + + /** + * Mock responder for Get/Update/Delete. Given an existing ID, + * extracts the data from that location and either just sends + * it back, or manipulates it as requested. + */ + var rudResolver = function (method, url, body) { + $log.info('[mock] ' + method + ' ' + url); + + if (!!body) { + body = JSON.parse(body); + } + + var id = parseInt(crudMatcher.test(url).id); + var idx = getIndexById(defaultData, idParamName, id); + var now = Math.round(new Date().getTime() / 1000); + + if (idx === false) { + return [404]; + } + + // Temporarily disable the camelcase JSHint rule. + // jshint -W106 + switch (method) { + case 'GET': + return [200, defaultData[idx]]; + case 'PUT': + body.id = id; + body.updated_at = now; + defaultData[idx] = body; + return [200, defaultData[idx]]; + case 'DELETE': + defaultData.splice(idx, 1); + return [200]; + } + // Re-enable camelcase check. + // jshint +W106 + }; + + $httpBackend.when('POST', createMatcher) + .respond(createResolver); + $httpBackend.when('GET', crudMatcher) + .respond(rudResolver); + $httpBackend.when('PUT', crudMatcher) + .respond(rudResolver); + $httpBackend.when('DELETE', crudMatcher) + .respond(rudResolver); + } + }; + }); \ No newline at end of file diff --git a/src/app/services/mock/project_mock.js b/src/app/services/mock/project_mock.js new file mode 100644 index 00000000..1d411349 --- /dev/null +++ b/src/app/services/mock/project_mock.js @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * Mock resource responses for the Project resource. + */ + +angular.module('sb.services') + .run(function (mock) { + 'use strict'; + + var projects = [ + { + 'id': 0, + 'created_at': 12000000, + 'updated_at': 12000000, + 'name': 'Test Project 1', + 'description': 'Let\'s make orange juice', + 'team_id': null + }, + { + 'id': 1, + 'created_at': 12000000, + 'updated_at': 12000000, + 'name': 'Test Project 2', + 'description': 'Let\'s make apple juice', + 'team_id': null + }, + { + 'id': 2, + 'created_at': 12000000, + 'updated_at': 12000000, + 'name': 'Test Project 3', + 'description': 'Let\'s make lemonade', + 'team_id': null + } + ]; + + mock.api('/api/v1/projects', + '/api/v1/projects/:id', + 'id', + projects); + } +) +; \ No newline at end of file diff --git a/src/app/services/mock/task_mock.js b/src/app/services/mock/task_mock.js new file mode 100644 index 00000000..71b34474 --- /dev/null +++ b/src/app/services/mock/task_mock.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * Mock resource responses for the Task resource. + */ + +angular.module('sb.services') + .run(function (mock) { + 'use strict'; + + var tasks = [ + ]; + + mock.api('/api/v1/tasks', + '/api/v1/tasks/:id', + 'id', + tasks); + } +) +; \ No newline at end of file diff --git a/src/app/services/mock/team.js b/src/app/services/mock/team.js new file mode 100644 index 00000000..1cfc2e26 --- /dev/null +++ b/src/app/services/mock/team.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * Mock resource responses for the Team resource. + */ + +angular.module('sb.services') + .run(function (mock) { + 'use strict'; + + var team = [ + ]; + + mock.api('/api/v1/teams', + '/api/v1/teams/:id', + 'id', + team); + } +) +; \ No newline at end of file diff --git a/src/app/services/mock/user_mock.js b/src/app/services/mock/user_mock.js new file mode 100644 index 00000000..0d1f8369 --- /dev/null +++ b/src/app/services/mock/user_mock.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * Mock resource responses for the User resource. + */ + +angular.module('sb.services') + .run(function (mock) { + 'use strict'; + + var users = [ + ]; + + mock.api('/api/v1/users', + '/api/v1/users/:id', + 'id', + users); + } +) +; \ No newline at end of file diff --git a/src/app/services/module.js b/src/app/services/module.js index 12ca5cda..135ea440 100644 --- a/src/app/services/module.js +++ b/src/app/services/module.js @@ -21,4 +21,5 @@ * * @author Michael Krotscheck */ -angular.module('sb.services', ['ngResource', 'ngCookies', 'ngMockE2E']); \ No newline at end of file +angular.module('sb.services', ['ngResource', 'ngCookies', 'ngMock', + 'ui.router']); \ No newline at end of file diff --git a/src/app/services/provider/storyboard_api_signature.js b/src/app/services/provider/storyboard_api_signature.js new file mode 100644 index 00000000..97e41dc9 --- /dev/null +++ b/src/app/services/provider/storyboard_api_signature.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * In lieu of extension, here we're injecting our common API signature that + * can be reused by all of our services. + * + * @author Michael Krotscheck + */ +angular.module('sb.services') + .factory('storyboardApiSignature', function () { + 'use strict'; + + return { + 'create': { + method: 'POST' + }, + 'read': { + method: 'GET', + cache: false + }, + 'update': { + method: 'PUT' + }, + 'delete': { + method: 'DELETE' + }, + 'search': { + method: 'GET', + isArray: true, + transformResponse: function (data) { + if (data.error) { + return data; + } else { + return data.results; + } + } + } + }; + } +); \ No newline at end of file diff --git a/src/app/services/resource/auth_provider.js b/src/app/services/resource/auth_provider.js index b6d9a29a..4a4411ec 100644 --- a/src/app/services/resource/auth_provider.js +++ b/src/app/services/resource/auth_provider.js @@ -24,35 +24,10 @@ */ angular.module('sb.services').factory('AuthProvider', - function ($resource, storyboardApiBase) { + function ($resource, storyboardApiBase, storyboardApiSignature) { 'use strict'; return $resource(storyboardApiBase + '/auth/provider/:id', {id: '@id'}, - { - 'create': { - method: 'POST' - }, - 'get': { - method: 'GET', - cache: true - }, - 'save': { - method: 'PUT' - }, - 'delete': { - method: 'DELETE' - }, - 'query': { - method: 'GET', - isArray: true, - transformResponse: function (data) { - if (data.error) { - return data; - } else { - return data.results; - } - } - } - }); + storyboardApiSignature); }); \ No newline at end of file diff --git a/src/app/services/resource/project.js b/src/app/services/resource/project.js new file mode 100644 index 00000000..cc128de9 --- /dev/null +++ b/src/app/services/resource/project.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * The angular resource abstraction that allows us to access projects and their + * details. + * + * @see storyboardApiSignature + * @author Michael Krotscheck + */ +angular.module('sb.services').factory('Project', + function ($resource, storyboardApiBase, storyboardApiSignature) { + 'use strict'; + + return $resource(storyboardApiBase + '/projects/:id', + {id: '@id'}, + storyboardApiSignature); + }); \ No newline at end of file diff --git a/src/app/services/resource/project_group.js b/src/app/services/resource/project_group.js new file mode 100644 index 00000000..652056f7 --- /dev/null +++ b/src/app/services/resource/project_group.js @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * The angular resource abstraction that allows us to access projects groups. + * + * @see storyboardApiSignature + * @author Michael Krotscheck + */ +angular.module('sb.services').factory('ProjectGroup', + function ($resource, storyboardApiBase, storyboardApiSignature) { + 'use strict'; + + return $resource(storyboardApiBase + '/project_groups/:id', + {id: '@id'}, + storyboardApiSignature); + }); + + +/* + This is initial commit adding pecan/wsme framework. + Example operations are: + * GET /v1/project_groups + * GET /v1/project_groups/ + + * GET /v1/projects + * GET /v1/projects/ + + * GET /v1/teams + * GET /v1/teams/ + * POST /v1/teams + + * GET /v1/users + * GET /v1/users/ + * POST /v1/users + * PUT /v1/users/ + */ \ No newline at end of file diff --git a/src/app/services/resource/task.js b/src/app/services/resource/task.js new file mode 100644 index 00000000..91a11b60 --- /dev/null +++ b/src/app/services/resource/task.js @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * The angular resource abstraction that allows us to create and modify tasks. + * + * @see storyboardApiSignature + * @author Michael Krotscheck + */ +angular.module('sb.services').factory('Task', + function ($resource, storyboardApiBase, storyboardApiSignature) { + 'use strict'; + + return $resource(storyboardApiBase + '/tasks/:id', + {id: '@id'}, + storyboardApiSignature); + }); \ No newline at end of file diff --git a/src/app/services/resource/team.js b/src/app/services/resource/team.js new file mode 100644 index 00000000..39e341a6 --- /dev/null +++ b/src/app/services/resource/team.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * The angular resource abstraction that allows us to access teams and their + * details. + * + * @see storyboardApiSignature + * @author Michael Krotscheck + */ +angular.module('sb.services').factory('Team', + function ($resource, storyboardApiBase, storyboardApiSignature) { + 'use strict'; + + return $resource(storyboardApiBase + '/teams/:id', + {id: '@id'}, + storyboardApiSignature); + }); \ No newline at end of file diff --git a/src/app/services/resource/user.js b/src/app/services/resource/user.js new file mode 100644 index 00000000..598f7046 --- /dev/null +++ b/src/app/services/resource/user.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * The angular resource abstraction that allows us to search, access, and + * modify users. + * + * @see storyboardApiSignature + * @author Michael Krotscheck + */ +angular.module('sb.services').factory('User', + function ($resource, storyboardApiBase, storyboardApiSignature) { + 'use strict'; + + return $resource(storyboardApiBase + '/users/:id', + {id: '@id'}, + storyboardApiSignature); + }); \ No newline at end of file diff --git a/src/app/templates/auth/provider/login.html b/src/app/templates/auth/provider/login.html index f5967fe1..1d6b94f4 100644 --- a/src/app/templates/auth/provider/login.html +++ b/src/app/templates/auth/provider/login.html @@ -18,6 +18,10 @@

Login with {{authProvider.title}}

+

+ This feature requires the existence of a functioning API + Authentication layer, and is therefore disabled. +

\ No newline at end of file diff --git a/src/app/templates/project/detail.html b/src/app/templates/project/detail.html new file mode 100644 index 00000000..2bcdb64b --- /dev/null +++ b/src/app/templates/project/detail.html @@ -0,0 +1,38 @@ + +
+
+

+ +

+
+
+ +
+
+
+

{{project.name}}

+ +

{{project.description}}

+
+
+
+
+
+ Project Detail List TBD. +
+
+
diff --git a/src/app/templates/project/edit.html b/src/app/templates/project/edit.html new file mode 100644 index 00000000..8b109241 --- /dev/null +++ b/src/app/templates/project/edit.html @@ -0,0 +1,84 @@ + +
+
+

+ +

+
+
+ +
+
+
+

Project: {{project.name}}

+
+
+
+
+
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+ + + + Cancel + +
+
+
+
+
+
diff --git a/src/app/templates/project/list.html b/src/app/templates/project/list.html index d91f54d3..11ab3ab1 100644 --- a/src/app/templates/project/list.html +++ b/src/app/templates/project/list.html @@ -16,28 +16,67 @@
-
- - - - +
+

+ + + + Projects +

+
-
-

Projects

- - - - - - - +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
NameTitle
+ + +
+
+ + + +
+ + {{project.name}} + +
+ + {{project.description}} +
diff --git a/src/app/templates/project/new.html b/src/app/templates/project/new.html new file mode 100644 index 00000000..5c3c3997 --- /dev/null +++ b/src/app/templates/project/new.html @@ -0,0 +1,73 @@ + + +
+
+
+

Create a new project

+
+
+
+
+
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+ + + Cancel + +
+
+
+
+
+
diff --git a/src/styles/bootstrap_addons.less b/src/styles/bootstrap_addons.less new file mode 100644 index 00000000..fd25deed --- /dev/null +++ b/src/styles/bootstrap_addons.less @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * Generic overrides and addons for bootstrap. + */ +h1, h2, h3, h4, h5, h6 { + &.no-margin { + margin: 0px; + } +} \ No newline at end of file diff --git a/src/styles/main.less b/src/styles/main.less index 125223a0..30fe5443 100644 --- a/src/styles/main.less +++ b/src/styles/main.less @@ -25,6 +25,7 @@ @import './font-awesome.less'; // Custom variable overrides @import './bootstrap_variable_overrides.less'; +@import './bootstrap_addons.less'; // Add our own custom icon font. @import './custom_font_icons.less'; // Module specific styles diff --git a/test/functional/auth/auth_route.js b/test/functional/auth/auth_route.js index 0598c6b0..c6179816 100644 --- a/test/functional/auth/auth_route.js +++ b/test/functional/auth/auth_route.js @@ -25,6 +25,5 @@ describe('Storyboard Login Routes', function () { it('should redirect /auth/provider to /auth/provider/list', function () { browser.get('http://localhost:9000/#!/auth/provider'); expect(browser.getCurrentUrl()).toContain('#!/auth/provider/list'); - }); }); diff --git a/test/unit/services/http/http_error_broadcaster_test.js b/test/unit/services/http/http_error_broadcaster_test.js new file mode 100644 index 00000000..ea61449e --- /dev/null +++ b/test/unit/services/http/http_error_broadcaster_test.js @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * This test suite ensures that HTTP response codes are successfully captured + * and broadcast to the system + */ +describe('httpErrorBroadcaster', function () { + 'use strict'; + + var $rootScope, $httpBackend, $http, $resource, MockResource, + storyboardApiBase; + + var errorResponse = { + error_code: 404, + error_message: 'This is an error message' + }; + + // Setup + beforeEach(function () { + // Load the module under test + module('sb.services'); + + inject(function ($injector) { + // Capture various providers for later use. + $rootScope = $injector.get('$rootScope'); + $http = $injector.get('$http'); + $httpBackend = $injector.get('$httpBackend'); + $resource = $injector.get('$resource'); + MockResource = $resource('/foo/:id', {id: '@id'}); + storyboardApiBase = $injector.get('storyboardApiBase'); + }); + + // Start listening to the broadcast method. + spyOn($rootScope, '$broadcast'); + }); + + // Teardown + afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + + it('should capture events on the $rootScope', function () { + + $httpBackend.when('GET', '/foo/99') + .respond(553, JSON.stringify(errorResponse)); + + var complete = false; + + runs(function () { + MockResource.get({'id': 99}, + function () { + complete = true; + }, + function () { + complete = true; + }); + + $httpBackend.flush(); + }); + + waitsFor(function () { + return complete; + }, 'query to complete', 5000); + + runs(function () { + expect($rootScope.$broadcast) + .toHaveBeenCalledWith('http_553', errorResponse); + }); + }); +}); diff --git a/test/unit/services/provider/storyboard_api_base_test.js b/test/unit/services/provider/storyboard_api_base_test.js new file mode 100644 index 00000000..4d6e13a7 --- /dev/null +++ b/test/unit/services/provider/storyboard_api_base_test.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * This test suite verifies that our API base URI is detected and deferred + * as expected. + */ +describe('storyboardApiBase', function () { + 'use strict'; + + it('should default to /api/v1', function () { + module('sb.services'); + + inject(function (storyboardApiBase) { + expect(storyboardApiBase).toEqual('/api/v1'); + }); + }); + + it('should detect a value in window.ENV', function () { + window.ENV = { + storyboardApiBase: 'https://localhost:8080/api/v1' + }; + + module('sb.services'); + + inject(function (storyboardApiBase) { + expect(storyboardApiBase).toEqual('https://localhost:8080/api/v1'); + }); + + delete window.ENV; + }); + + it('should defer to properties injected at the parent level.', function () { + angular.module('testModule', ['sb.services']) + .config(function ($provide) { + $provide.constant('storyboardApiBase', 'spam.eggs.com'); + }); + + module('testModule'); + + inject(function (storyboardApiBase) { + expect(storyboardApiBase).toEqual('spam.eggs.com'); + }); + }); + +}); diff --git a/test/unit/services/provider/storyboard_api_signature_test.js b/test/unit/services/provider/storyboard_api_signature_test.js new file mode 100644 index 00000000..87328906 --- /dev/null +++ b/test/unit/services/provider/storyboard_api_signature_test.js @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * This test suite verifies that our default API request signature is + * sane. + */ +describe('storyboardApiSignature', function () { + 'use strict'; + + beforeEach(module('sb.services')); + + it('should exist', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature).toBeTruthy(); + }); + }); + + it('should declare CRUD methods', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.create).toBeTruthy(); + expect(storyboardApiSignature.read).toBeTruthy(); + expect(storyboardApiSignature.update).toBeTruthy(); + expect(storyboardApiSignature.delete).toBeTruthy(); + }); + }); + + it('should declare a search method', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.search).toBeTruthy(); + }); + }); + + it('should use POST to create', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.create).toBeTruthy(); + expect(storyboardApiSignature.create.method).toEqual('POST'); + }); + }); + it('should use GET to read', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.read).toBeTruthy(); + expect(storyboardApiSignature.read.method).toEqual('GET'); + }); + }); + it('should use PUT to update', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.update).toBeTruthy(); + expect(storyboardApiSignature.update.method).toEqual('PUT'); + }); + }); + it('should use DELETE to delete', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.delete).toBeTruthy(); + expect(storyboardApiSignature.delete.method).toEqual('DELETE'); + }); + }); + it('should use GET to search', function () { + inject(function (storyboardApiSignature) { + expect(storyboardApiSignature.search).toBeTruthy(); + expect(storyboardApiSignature.search.method).toEqual('GET'); + }); + }); + + it('should properly construct a resource', function () { + inject(function (storyboardApiSignature, $resource) { + + var Resource = $resource('/path/:id', + {id: '@id'}, + storyboardApiSignature); + expect(Resource.search).toBeTruthy(); + expect(Resource.read).toBeTruthy(); + + var resourceInstance = new Resource(); + expect(resourceInstance.$create).toBeTruthy(); + expect(resourceInstance.$update).toBeTruthy(); + expect(resourceInstance.$delete).toBeTruthy(); + }); + }); +});