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
This commit is contained in:
Michael Krotscheck 2014-01-16 19:12:54 -08:00
parent e9c34dec57
commit 32283d03cc
32 changed files with 1371 additions and 95 deletions

View File

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

View File

@ -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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,4 +21,5 @@
*
* @author Michael Krotscheck
*/
angular.module('sb.services', ['ngResource', 'ngCookies', 'ngMockE2E']);
angular.module('sb.services', ['ngResource', 'ngCookies', 'ngMock',
'ui.router']);

View File

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

View File

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

View File

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

View File

@ -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/<group_name>
* GET /v1/projects
* GET /v1/projects/<project_name>
* GET /v1/teams
* GET /v1/teams/<team_name>
* POST /v1/teams
* GET /v1/users
* GET /v1/users/<username>
* POST /v1/users
* PUT /v1/users/<username>
*/

View File

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

View File

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

View File

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

View File

@ -18,6 +18,10 @@
<div class="row">
<div class="col-xs-12">
<h1>Login with {{authProvider.title}}</h1>
<p class="lead">
This feature requires the existence of a functioning API
Authentication layer, and is therefore disabled.
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,38 @@
<!--
~ 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.
-->
<div class="container" ng-show="isLoading">
<div class="col-xs-12">
<p class="text-center">
<i class="fa fa-refresh fa-spin"></i>
</p>
</div>
</div>
<div class="container" ng-hide="isLoading">
<div class="row">
<div class="col-xs-12">
<h1>{{project.name}}</h1>
<p>{{project.description}}</p>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12">
Project Detail List TBD.
</div>
</div>
</div>

View File

@ -0,0 +1,84 @@
<!--
~ 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.
-->
<div class="container" ng-show="isLoading">
<div class="col-xs-12">
<p class="text-center">
<i class="fa fa-refresh fa-2x fa-spin"></i>
</p>
</div>
</div>
<div class="container" ng-hide="isLoading">
<div class="row">
<div class="col-xs-12">
<h1>Project: {{project.name}}</h1>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="projectForm">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
Project Name:
</label>
<div class="col-sm-10">
<input id="name"
type="text"
class="form-control"
ng-model="project.name"
required
placeholder="Project Name">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">
Project Description
</label>
<div class="col-sm-10">
<textarea id="description"
class="form-control"
ng-model="project.description"
required
placeholder="A brief project description">
</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button"
ng-click="update()"
class="btn btn-primary"
ng-disabled="!projectForm.$valid">
Save Changes
</button>
<button ng-click="remove()"
class="btn btn-danger">
Delete Project
</button>
<a href="#!/project/list"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -16,28 +16,67 @@
<div class="container">
<div class="row">
<div class="col-sm-2">
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li class="nav-header">Projects</li>
<li class="disabled"><a href="#">Search projects</a></li>
</ul>
</div>
<!--/.well -->
<div class="col-sm-8 col-md-9">
<h3 class="no-margin">
<a href="#!/project/new" class="btn btn-default">
<i class="fa fa-plus"></i>
</a>
Projects
</h3>
<br class="visible-xs"/>
</div>
<div class="col-sm-10">
<h3>Projects</h3>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Title</th>
</tr>
</thead>
<div class="col-sm-4 col-md-3">
<div class="input-group">
<input type="text" class="form-control"
placeholder="Search Projects"
ng-disabled="isSearching"
ng-enter="search()"
ng-model="searchQuery"/>
<span class="input-group-btn">
<button type="button" ng-click="search()"
ng-disabled="isSearching"
class="btn btn-default">
<i class="fa fa-refresh fa-spin"
ng-show="isSearching"></i>
<i class="fa fa-search"
ng-hide="isSearching"></i>
</button>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<hr/>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div ng-show="isSearching">
<hr/>
<p class="text-center">
<i class="fa fa-refresh fa-spin fa-lg"></i>
</p>
</div>
<table class="table table-striped table-hover table-responsive"
ng-hide="isSearching">
<tbody>
<tr ng-repeat="project in projects">
<td>
<div class="pull-right">
<a href="#!/project/{{project.id}}/edit">
<i class="fa fa-edit"></i>
</a>
</div>
<a href="#!/project/{{project.id}}">
<strong>{{project.name}}</strong>
</a>
<br/>
{{project.description}}
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,73 @@
<!--
~ 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.
-->
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Create a new project</h1>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="projectForm">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
Project Name:
</label>
<div class="col-sm-10">
<input id="name"
type="text"
class="form-control"
ng-model="newProject.name"
required
placeholder="Project Name">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">
Project Description
</label>
<div class="col-sm-10">
<textarea id="description"
class="form-control"
ng-model="newProject.description"
required
placeholder="A brief project description">
</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button"
ng-click="createProject()"
class="btn btn-primary"
ng-disabled="!projectForm.$valid">
Create project
</button>
<a href="#!/project/list"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>
</div>

24
src/styles/bootstrap_addons.less vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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