diff --git a/dashboard-project-api/.editorconfig b/dashboard-project-api/.editorconfig
new file mode 100644
index 0000000..3ee22e5
--- /dev/null
+++ b/dashboard-project-api/.editorconfig
@@ -0,0 +1,13 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# http://editorconfig.org
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/dashboard-project-api/.gitignore b/dashboard-project-api/.gitignore
new file mode 100644
index 0000000..deac3d2
--- /dev/null
+++ b/dashboard-project-api/.gitignore
@@ -0,0 +1,20 @@
+*.csv
+*.dat
+*.iml
+*.log
+*.out
+*.pid
+*.seed
+*.sublime-*
+*.swo
+*.swp
+*.tgz
+*.xml
+.DS_Store
+.idea
+.project
+.strong-pm
+coverage
+node_modules
+npm-debug.log
+js/
diff --git a/dashboard-project-api/.jshintignore b/dashboard-project-api/.jshintignore
new file mode 100644
index 0000000..ee8c771
--- /dev/null
+++ b/dashboard-project-api/.jshintignore
@@ -0,0 +1,2 @@
+/client/
+/node_modules/
diff --git a/dashboard-project-api/.jshintrc b/dashboard-project-api/.jshintrc
new file mode 100644
index 0000000..feb0928
--- /dev/null
+++ b/dashboard-project-api/.jshintrc
@@ -0,0 +1,21 @@
+{
+ "node": true,
+ "esnext": true,
+ "bitwise": true,
+ "camelcase": true,
+ "eqeqeq": true,
+ "eqnull": true,
+ "immed": true,
+ "indent": 2,
+ "latedef": "nofunc",
+ "newcap": true,
+ "nonew": true,
+ "noarg": true,
+ "quotmark": "single",
+ "regexp": true,
+ "undef": true,
+ "unused": false,
+ "trailing": true,
+ "sub": true,
+ "maxlen": 80
+}
diff --git a/dashboard-project-api/.yo-rc.json b/dashboard-project-api/.yo-rc.json
new file mode 100644
index 0000000..02f3fc1
--- /dev/null
+++ b/dashboard-project-api/.yo-rc.json
@@ -0,0 +1,3 @@
+{
+ "generator-loopback": {}
+}
\ No newline at end of file
diff --git a/dashboard-project-api/README.md b/dashboard-project-api/README.md
new file mode 100644
index 0000000..866baed
--- /dev/null
+++ b/dashboard-project-api/README.md
@@ -0,0 +1,3 @@
+# My Application
+
+The project is generated by [LoopBack](http://loopback.io).
\ No newline at end of file
diff --git a/dashboard-project-api/client/README.md b/dashboard-project-api/client/README.md
new file mode 100644
index 0000000..dd00c9e
--- /dev/null
+++ b/dashboard-project-api/client/README.md
@@ -0,0 +1,3 @@
+## Client
+
+This is the place for your application front-end files.
diff --git a/dashboard-project-api/common/models/blueprint.js b/dashboard-project-api/common/models/blueprint.js
new file mode 100644
index 0000000..820be0c
--- /dev/null
+++ b/dashboard-project-api/common/models/blueprint.js
@@ -0,0 +1,19 @@
+module.exports = function(Blueprint) {
+
+ Blueprint.url = function(projectName, blueprintName, cb) {
+ var url = 'https://blueprints.launchpad.net/' + projectName + '/+spec/' + blueprintName;
+ cb(null, url);
+ }
+
+ Blueprint.remoteMethod(
+ 'url',
+ {
+ accepts: [
+ {arg: 'projectName', type: 'string', required: true},
+ {arg: 'blueprintName', type: 'string', required: true}
+ ],
+ http: {verb: 'get'},
+ returns: {arg: 'url', type: 'string'}
+ }
+ );
+};
diff --git a/dashboard-project-api/common/models/blueprint.json b/dashboard-project-api/common/models/blueprint.json
new file mode 100644
index 0000000..5fa984b
--- /dev/null
+++ b/dashboard-project-api/common/models/blueprint.json
@@ -0,0 +1,16 @@
+{
+ "name": "Blueprint",
+ "base": "PersistedModel",
+ "idInjection": true,
+ "options": {},
+ "properties": {
+ "url": {
+ "type": "string",
+ "required": true
+ }
+ },
+ "validations": [],
+ "relations": {},
+ "acls": [],
+ "methods": {}
+}
diff --git a/dashboard-project-api/common/models/patch.js b/dashboard-project-api/common/models/patch.js
new file mode 100644
index 0000000..07ba406
--- /dev/null
+++ b/dashboard-project-api/common/models/patch.js
@@ -0,0 +1,51 @@
+'strict mode';
+var async = require('async');
+
+module.exports = function(Patch) {
+ Patch.afterRemote('list', function (ctx, response, next) {
+ var data = [];
+
+ response = JSON.parse(response.substring(5));
+ async.waterfall([
+ function createOutput(next){
+ response.forEach( function each (element){
+ data.push(
+ {
+ url: "https://review.openstack.org/#/c/" + element._number,
+ name: element.subject
+ });
+ });
+ next();
+ },
+ function sendData (next) {
+ ctx.result = data;
+ next;
+ }
+ ], next);
+ next();
+ });
+
+ Patch.afterRemote('latestUpdate', function (ctx, response, next) {
+ var data = [];
+
+ response = JSON.parse(response.substring(5));
+ async.waterfall([
+ function createOutput(next){
+ response.forEach( function each (element){
+ data.push(
+ {
+ latestDate: element.updated,
+ });
+ });
+ next();
+ },
+ function sendData (next) {
+ data = data.sort();
+ var lastUpdate = data.shift();
+ ctx.result = lastUpdate;
+ next;
+ }
+ ], next);
+ next();
+ });
+};
diff --git a/dashboard-project-api/common/models/patch.json b/dashboard-project-api/common/models/patch.json
new file mode 100644
index 0000000..33c440b
--- /dev/null
+++ b/dashboard-project-api/common/models/patch.json
@@ -0,0 +1,16 @@
+{
+ "name": "Patch",
+ "base": "PersistedModel",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "patchValues": {
+ "type": "object"
+ }
+ },
+ "validations": [],
+ "relations": {},
+ "acls": [],
+ "methods": {}
+}
diff --git a/dashboard-project-api/common/models/rst.js b/dashboard-project-api/common/models/rst.js
new file mode 100644
index 0000000..4162515
--- /dev/null
+++ b/dashboard-project-api/common/models/rst.js
@@ -0,0 +1,10 @@
+'strict mode';
+var async = require('async');
+
+module.exports = function(Rst) {
+
+ Rst.afterRemote('list', function (ctx, response, next) {
+ next();
+ });
+
+};
diff --git a/dashboard-project-api/common/models/rst.json b/dashboard-project-api/common/models/rst.json
new file mode 100644
index 0000000..5709236
--- /dev/null
+++ b/dashboard-project-api/common/models/rst.json
@@ -0,0 +1,17 @@
+{
+ "name": "Rst",
+ "plural": "Rsts",
+ "base": "PersistedModel",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "patchValues": {
+ "type": "object"
+ }
+ },
+ "validations": [],
+ "relations": {},
+ "acls": [],
+ "methods": {}
+}
diff --git a/dashboard-project-api/common/models/userStory.js b/dashboard-project-api/common/models/userStory.js
new file mode 100644
index 0000000..1bf804f
--- /dev/null
+++ b/dashboard-project-api/common/models/userStory.js
@@ -0,0 +1,419 @@
+module.exports = function(UserStory) {
+ var fs = require("fs");
+ var route = '../tracker';
+ var app = require('../../server/server');
+ var http = require("http");
+ var https = require('https');
+ var markdown = require("markdown").markdown;
+ var async = require("async");
+ var htmlparser = require("htmlparser");
+ var cheerio = require('cheerio');
+
+
+ var blueprintsResume = [];
+
+
+ var getAllfiles = function(getPercentage){
+
+ return fs.readdirSync(route)
+ .map(function(file){
+ var route = "../tracker/" + file ;
+ var data = JSON.parse(fs.readFileSync(route));
+ var parsedData;
+
+ if(getPercentage){
+ parsedData = getTotalPercentage(data);
+ data.blueprintList = parsedData;
+ }
+
+ return data;
+ });
+
+ }
+
+ //get completed percentage of all the userstories
+ var getTotalPercentage = function(userStory){
+
+ blueprintsResume.completed = 0;
+ blueprintsResume.total = 0;
+ var blueprintList = [];
+
+ userStory.tasks.forEach(function (taskName, index, array) {
+
+ var task = userStory.tasks_status[taskName];
+
+ task.projects.forEach(function (projectName, index, array) {
+
+ var blueprints = task.projects_status[projectName].blueprints;
+ var blueprintNames = Object.keys(blueprints);
+ blueprintList.push(blueprintNames);
+
+ blueprintNames.forEach(function (blueprintName, index, array) {
+
+ if (blueprints[blueprintName] == 'completed')
+ blueprintsResume.completed = blueprintsResume.completed + 1;
+
+ blueprintsResume.total = blueprintsResume.total + 1;
+
+ })
+
+ })
+
+ })
+
+ var finalPercentage = Math.round((blueprintsResume.completed*100)/blueprintsResume.total)
+ blueprintList.push([{percentage:(finalPercentage)}]);
+ return blueprintList;
+
+ }
+
+ //get completed percentage of a User story
+ var getPercentage = function(blueprints){
+ var total = blueprints.length;
+ var complete = 0;
+
+ blueprints.forEach(function(element, index, array) {
+
+ if(element == 'completed')
+ complete = complete + 1;
+ })
+ return Math.round((complete*100)/total);
+
+ }
+
+ var getFileById = function(id){
+
+ //get all files
+ var userStories = getAllfiles();
+ //filter by Id
+ var file = userStories.filter(function(item){
+ return item.id == id;
+ })
+
+ if(file.length > 0){
+ file = file[0];
+ }else{
+ file = 'The file with id: '+ id +' does not exist.'
+ }
+ return file;
+
+ };
+
+ var getDetailedUri = function(source) {
+ var base = 'http://specs.openstack.org/openstack/openstack-user-stories/user-stories/proposed/';
+
+ return base + source + '.html';
+
+ }
+
+ var parseUserStory = function(id){
+ var data = [];
+ return function(callback){
+ var file = getFileById(id);
+
+ var Patch = app.models.Patch;
+
+ Patch.latestUpdate(file.source, function(err, response) {
+
+ var lastUpdate = '';
+
+ if(response){
+ response = JSON.parse(response.substring(5));
+ response.forEach( function each (element){
+ data.push(
+ {
+ latestDate: element.updated,
+ });
+ });
+
+ data = data.sort();
+ lastUpdate = data.shift();
+
+ if(lastUpdate){
+ lastUpdate = lastUpdate.latestDate;
+ }
+ }
+
+ var userStory = {
+ title:file.description,
+ description:'',
+ status:file.status,
+ showDetailedUri:getDetailedUri(file.source),
+ submittedBy:file.submitted_by.name,
+ submittedByEmail:file.submitted_by.email,
+ createdOn:file.date,
+ updatedOn:lastUpdate,
+ id:file.id,
+ percentageComplete:''
+ };
+ callback(null, userStory, file.tasks, file.tasks_status);
+
+ })
+
+ }
+ }
+
+
+ var getTaskDescription = function(task, callback){
+
+ var Rst = app.models.Rst;
+ var spec = task['cross-project spec'] + '.rst'
+
+ Rst.list(spec, function(err, data){
+
+ var html_content = markdown.toHTML(data);
+ var $ = cheerio.load(html_content);
+
+ var index = null;
+ //Find the title
+ var description = $('h1').each(function(i, elem) {
+ if(elem.children[0].data == 'Problem description'){
+ index = i;
+ }
+ });
+
+ //get Text description
+ if(index != null){
+ description = $($('h1')[index]).next().text()
+ }else{
+ description = '';
+ }
+
+ callback(null, description);
+
+ })
+
+ }
+
+ var getUriTask = function(spec){
+ var base = 'https://github.com/openstack/openstack-specs/blob/master/specs/';
+
+ return base + spec + '.rst';
+ }
+
+ var parseTask = function(originalTask, callback){
+
+ getTaskDescription(originalTask, function(err, description){
+ originalTask.description = description;
+ originalTask.url = getUriTask(originalTask['cross-project spec']);
+ originalTask.xp_status = originalTask.xp_status;
+ callback(null, originalTask)
+ })
+
+ }
+
+
+ var parseProject = function(originalProject, callback){
+
+ var urlArray = originalProject.spec.split('/');
+ var nameArray = urlArray[urlArray.length-1].split('.')
+ originalProject.spec_name = nameArray[0];
+
+ callback(null, originalProject)
+ }
+
+
+ var parseBlueprint = function(originalBlueprint, blueprintName, projectName, callback){
+ var Blueprint = app.models.Blueprint;
+ var Patch = app.models.Patch;
+
+ var status = originalBlueprint;
+
+ Blueprint.url(projectName, blueprintName, function(err, uri){
+
+ blueprintsResume.push(status)
+
+ blueprintsResume.complete = blueprintsResume.total +1 ;
+
+
+ Patch.list(blueprintName, function(err, response) {
+
+ var data = [];
+
+ response = JSON.parse(response.substring(5));
+
+ response.forEach(function each(element) {
+ data.push(
+ {
+ url: "https://review.openstack.org/#/c/" + element._number,
+ name: element.subject
+ });
+ });
+
+ originalBlueprint = {
+ name: blueprintName.replace(/-/g, " "),
+ uri: uri,
+ status: status,
+ review_link:data
+ }
+ callback(null, originalBlueprint)
+
+ })
+
+ })
+
+ }
+
+
+ //TODO: APPLY async.waterfall
+ var parseTasks = function(userStory, tasksNames, tasks, callback) { //get tasks
+
+ var tmpTasks = {};
+ async.each(tasksNames, function(taskName, callbackInner) {
+
+ parseTask(tasks[taskName], function(err, parsedTask){
+
+ tmpTasks[taskName] = parsedTask;
+
+ var tmpProjects = {};
+ async.each(tmpTasks[taskName].projects, function(projectName, callbackInner2) {
+
+ parseProject(tmpTasks[taskName].projects_status[projectName], function(err, parsedProject){
+ tmpProjects[projectName] = parsedProject;
+
+ //Bluprints
+ var blueprintNames = Object.keys(tmpProjects[projectName].blueprints);
+
+ async.map(blueprintNames, function(blueprintName, callbackInner3) {
+
+ parseBlueprint(tmpProjects[projectName].blueprints[blueprintName], blueprintName, projectName, function(err, parsedBlueprint){
+ // tmpProjects[projectName].blueprints[blueprintName] = parsedBlueprint
+ callbackInner3(null, parsedBlueprint)
+ })
+
+ }, function(err, blueprints) {
+
+ tmpProjects[projectName].blueprints = blueprints;
+
+ var project = {};
+ project[projectName] = tmpProjects[projectName];
+
+ callbackInner2(null)
+
+ });
+ //fin blueprints
+
+ })
+
+ }, function(err) {
+
+ tmpTasks[taskName].projects_status = tmpProjects;
+ callbackInner(null)
+ });
+
+ })
+
+ }, function(err) {
+
+ userStory.tasks = tasksNames;
+ userStory.tasks_status = tmpTasks;
+
+ userStory.percentageComplete = getPercentage(blueprintsResume)
+
+ callback(null, userStory);
+
+ });
+
+ }
+
+ UserStory.on('attached',function(){
+
+ UserStory.findById = function(id, params, cb){
+
+ async.waterfall([
+ parseUserStory(id),
+ parseTasks
+ ], function (err, result) {
+ cb(null,result)
+ });
+
+ };
+
+ UserStory.find = function(params, cb){
+ var Patch = app.models.Patch;
+ //get all files
+ var userStories = getAllfiles(true);
+ var percentage;
+ var blueprintKeys;
+ var dateList = [];
+ //var response = userStories;
+
+ var result;
+
+ // looking dates
+ var lookingDates = function (id, blueprintKeys) {
+ blueprintKeys.forEach(function each (key) {
+ var data = [];
+ Patch.latestUpdate(key, function (err, response, next) {
+
+ response = JSON.parse(response.substring(5));
+ async.waterfall([
+ function createOutput(next){
+ response.forEach( function each (element){
+ data.push(
+ {
+ latestDate: element.updated,
+ });
+ });
+ next();
+ },
+ function sendData (next) {
+ data = data.sort();
+ var lastUpdate = data.shift();
+ if(lastUpdate !== undefined) {
+ dateList.push({
+ id: id,
+ value: lastUpdate.latestDate
+ });
+ }
+ next;
+ }
+ ], next);
+ });
+ });
+ }
+
+ function keysrt(key,asc) {
+ return function(a,b){
+ return asc ? ~~(a[key] < b[key]) : ~~(a[key] > b[key]);
+ }
+ }
+
+ async.waterfall ([
+ function readStories (next) {
+ userStories.forEach (function eachStory (story, x) {
+ //console.log(story.blueprintList);
+ //TODO change the array of percentage
+ percentage = (story.blueprintList).pop();
+ blueprintKeys = story.blueprintList;
+ result = lookingDates((story.id), blueprintKeys);
+ story.percentage = percentage[0].percentage;
+ });
+ next();
+ },
+ function resumeDate (next){
+ setTimeout(function(){
+ dateList = dateList.sort(keysrt('value'));
+ userStories.forEach (function eachStory (story) {
+ dateList.forEach (function eachDate (dateElement){
+ if(dateElement.id === story.id){
+ story.latestUpdate = dateList
+ }
+ });
+ });
+ cb(null,userStories);
+ }, 1000);
+
+ next;
+ },
+ function sendResult(next){
+ //cb(null,userStories);
+ next;
+ }
+ ]);
+
+
+ }
+ })
+
+};
diff --git a/dashboard-project-api/common/models/userStory.json b/dashboard-project-api/common/models/userStory.json
new file mode 100644
index 0000000..2d07b5f
--- /dev/null
+++ b/dashboard-project-api/common/models/userStory.json
@@ -0,0 +1,16 @@
+{
+ "name": "UserStory",
+ "base": "PersistedModel",
+ "idInjection": false,
+ "options": {},
+ "properties": {
+ "UserStory": {
+ "type": "array",
+ "required": false
+ }
+ },
+ "validations": [],
+ "relations": {},
+ "acls": [],
+ "methods": {}
+}
diff --git a/dashboard-project-api/package.json b/dashboard-project-api/package.json
new file mode 100644
index 0000000..48c7a6e
--- /dev/null
+++ b/dashboard-project-api/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "dashboard-project-api",
+ "version": "1.0.0",
+ "main": "server/server.js",
+ "scripts": {
+ "start": "node .",
+ "build-sdk": "mkdir js; lb-ng server/server.js js/lbServices.js",
+ "pretest": "jshint .",
+ "posttest": "nsp check"
+ },
+ "dependencies": {
+ "async": "^2.0.0-rc.3",
+ "cheerio": "^0.0.3",
+ "compression": "^1.0.3",
+ "cors": "^2.5.2",
+ "helmet": "~0.14.0",
+ "htmlparser": "^1.7.7",
+ "loopback": "^2.22.0",
+ "loopback-boot": "^2.6.5",
+ "loopback-component-explorer": "^2.4.0",
+ "loopback-connector-rest": "^1.5.0",
+ "loopback-datasource-juggler": "^2.39.0",
+ "markdown": "^0.5.0",
+ "serve-favicon": "^2.0.1",
+ "underscore": "^1.8.3"
+ },
+ "devDependencies": {
+ "jshint": "^2.5.6",
+ "nsp": "^2.1.0"
+ },
+ "repository": {
+ "type": "",
+ "url": ""
+ },
+ "license": "UNLICENSED",
+ "description": "dashboard-project-api"
+}
diff --git a/dashboard-project-api/server/boot/authentication.js b/dashboard-project-api/server/boot/authentication.js
new file mode 100644
index 0000000..a87cd08
--- /dev/null
+++ b/dashboard-project-api/server/boot/authentication.js
@@ -0,0 +1,4 @@
+module.exports = function enableAuthentication(server) {
+ // enable authentication
+ server.enableAuth();
+};
diff --git a/dashboard-project-api/server/boot/root.js b/dashboard-project-api/server/boot/root.js
new file mode 100644
index 0000000..e106142
--- /dev/null
+++ b/dashboard-project-api/server/boot/root.js
@@ -0,0 +1,6 @@
+module.exports = function(server) {
+ // Install a `/` route that returns server status
+ var router = server.loopback.Router();
+ router.get('/', server.loopback.status());
+ server.use(router);
+};
diff --git a/dashboard-project-api/server/component-config.json b/dashboard-project-api/server/component-config.json
new file mode 100644
index 0000000..f36959a
--- /dev/null
+++ b/dashboard-project-api/server/component-config.json
@@ -0,0 +1,5 @@
+{
+ "loopback-component-explorer": {
+ "mountPath": "/explorer"
+ }
+}
diff --git a/dashboard-project-api/server/config.json b/dashboard-project-api/server/config.json
new file mode 100644
index 0000000..38e008e
--- /dev/null
+++ b/dashboard-project-api/server/config.json
@@ -0,0 +1,27 @@
+{
+ "restApiRoot": "/api",
+ "host": "0.0.0.0",
+ "port": 3004,
+ "remoting": {
+ "context": {
+ "enableHttpContext": false
+ },
+ "rest": {
+ "normalizeHttpPath": false,
+ "xml": false
+ },
+ "json": {
+ "strict": false,
+ "limit": "100kb"
+ },
+ "urlencoded": {
+ "extended": true,
+ "limit": "100kb"
+ },
+ "cors": false,
+ "errorHandler": {
+ "disableStackTrace": false
+ }
+ },
+ "legacyExplorer": false
+}
diff --git a/dashboard-project-api/server/constants.js b/dashboard-project-api/server/constants.js
new file mode 100644
index 0000000..e69de29
diff --git a/dashboard-project-api/server/datasources.json b/dashboard-project-api/server/datasources.json
new file mode 100644
index 0000000..130ccc7
--- /dev/null
+++ b/dashboard-project-api/server/datasources.json
@@ -0,0 +1,47 @@
+{
+ "db": {
+ "name": "db",
+ "connector": "memory"
+ },
+ "Patch": {
+ "name": "Patch",
+ "connector": "rest",
+ "operations": [
+ {
+ "template": {
+ "method": "GET",
+ "url": "https://review.openstack.org/changes/?q=topic:{topic}",
+ "headers": {
+ "accepts": "application/json",
+ "content-type": "application/json"
+ }
+ },
+ "functions": {
+ "list": [
+ "topic"
+ ],
+ "latestUpdate": [
+ "topic"
+ ]
+ }
+ }
+ ]
+ },
+ "Rst": {
+ "name": "Rst",
+ "connector": "rest",
+ "operations": [
+ {
+ "template": {
+ "method": "GET",
+ "url": "https://raw.githubusercontent.com/openstack/openstack-specs/master/specs/{file}"
+ },
+ "functions": {
+ "list": [
+ "file"
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/dashboard-project-api/server/middleware.json b/dashboard-project-api/server/middleware.json
new file mode 100644
index 0000000..b2eb840
--- /dev/null
+++ b/dashboard-project-api/server/middleware.json
@@ -0,0 +1,50 @@
+{
+ "initial:before": {
+ "loopback#favicon": {}
+ },
+ "initial": {
+ "compression": {},
+ "cors": {
+ "params": {
+ "origin": true,
+ "credentials": true,
+ "maxAge": 86400
+ }
+ },
+ "helmet#xssFilter": {},
+ "helmet#frameguard": {
+ "params": [
+ "deny"
+ ]
+ },
+ "helmet#hsts": {
+ "params": {
+ "maxAge": 0,
+ "includeSubdomains": true
+ }
+ },
+ "helmet#hidePoweredBy": {},
+ "helmet#ieNoOpen": {},
+ "helmet#noSniff": {},
+ "helmet#noCache": {
+ "enabled": false
+ }
+ },
+ "session": {},
+ "auth": {},
+ "parse": {},
+ "routes": {
+ "loopback#rest": {
+ "paths": [
+ "${restApiRoot}"
+ ]
+ }
+ },
+ "files": {},
+ "final": {
+ "loopback#urlNotFound": {}
+ },
+ "final:after": {
+ "loopback#errorHandler": {}
+ }
+}
diff --git a/dashboard-project-api/server/middleware.production.json b/dashboard-project-api/server/middleware.production.json
new file mode 100644
index 0000000..4572b0c
--- /dev/null
+++ b/dashboard-project-api/server/middleware.production.json
@@ -0,0 +1,9 @@
+{
+ "final:after": {
+ "loopback#errorHandler": {
+ "params": {
+ "includeStack": false
+ }
+ }
+ }
+}
diff --git a/dashboard-project-api/server/model-config.json b/dashboard-project-api/server/model-config.json
new file mode 100644
index 0000000..473d888
--- /dev/null
+++ b/dashboard-project-api/server/model-config.json
@@ -0,0 +1,47 @@
+{
+ "_meta": {
+ "sources": [
+ "loopback/common/models",
+ "loopback/server/models",
+ "../common/models",
+ "./models"
+ ],
+ "mixins": [
+ "loopback/common/mixins",
+ "loopback/server/mixins",
+ "../common/mixins",
+ "./mixins"
+ ]
+ },
+ "UserStory": {
+ "dataSource": "db"
+ },
+ "AccessToken": {
+ "dataSource": "db",
+ "public": false
+ },
+ "ACL": {
+ "dataSource": "db",
+ "public": false
+ },
+ "RoleMapping": {
+ "dataSource": "db",
+ "public": false
+ },
+ "Role": {
+ "dataSource": "db",
+ "public": false
+ },
+ "Blueprint": {
+ "dataSource": "db",
+ "public": true
+ },
+ "Patch": {
+ "dataSource": "Patch",
+ "public": true
+ },
+ "Rst": {
+ "dataSource": "Rst",
+ "public": true
+ }
+}
diff --git a/dashboard-project-api/server/server.js b/dashboard-project-api/server/server.js
new file mode 100644
index 0000000..3be7b08
--- /dev/null
+++ b/dashboard-project-api/server/server.js
@@ -0,0 +1,27 @@
+var loopback = require('loopback');
+var boot = require('loopback-boot');
+
+var app = module.exports = loopback();
+
+app.start = function() {
+ // start the web server
+ return app.listen(function() {
+ app.emit('started');
+ var baseUrl = app.get('url').replace(/\/$/, '');
+ console.log('Web server listening at: %s', baseUrl);
+ if (app.get('loopback-component-explorer')) {
+ var explorerPath = app.get('loopback-component-explorer').mountPath;
+ console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
+ }
+ });
+};
+
+// Bootstrap the application, configure models, datasources and middleware.
+// Sub-apps like REST API are mounted via boot scripts.
+boot(app, __dirname, function(err) {
+ if (err) throw err;
+
+ // start the server if `$ node server.js`
+ if (require.main === module)
+ app.start();
+});
diff --git a/dashboard-project-app/.babelrc b/dashboard-project-app/.babelrc
new file mode 100755
index 0000000..bde6914
--- /dev/null
+++ b/dashboard-project-app/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["es2015"],
+ "plugins": ["transform-class-properties"]
+}
\ No newline at end of file
diff --git a/dashboard-project-app/.bowerrc b/dashboard-project-app/.bowerrc
new file mode 100755
index 0000000..76b30f4
--- /dev/null
+++ b/dashboard-project-app/.bowerrc
@@ -0,0 +1,4 @@
+{
+ "directory": "client/bower_components"
+
+}
diff --git a/dashboard-project-app/.buildignore b/dashboard-project-app/.buildignore
new file mode 100755
index 0000000..e69de29
diff --git a/dashboard-project-app/.editorconfig b/dashboard-project-app/.editorconfig
new file mode 100755
index 0000000..c2cdfb8
--- /dev/null
+++ b/dashboard-project-app/.editorconfig
@@ -0,0 +1,21 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+
+[*]
+
+# Change these settings to your own preference
+indent_style = space
+indent_size = 2
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/dashboard-project-app/.gitattributes b/dashboard-project-app/.gitattributes
new file mode 100755
index 0000000..d9a5563
--- /dev/null
+++ b/dashboard-project-app/.gitattributes
@@ -0,0 +1,43 @@
+* text=auto
+
+# These files are text and should be normalized (Convert crlf => lf)
+*.php text
+*.css text
+*.js text
+*.htm text
+*.html text
+*.xml text
+*.txt text
+*.ini text
+*.inc text
+.htaccess text
+
+# Denote all files that are truly binary and should not be modified.
+# (binary is a macro for -text -diff)
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.mov binary
+*.mp4 binary
+*.mp3 binary
+*.flv binary
+*.fla binary
+*.swf binary
+*.gz binary
+*.zip binary
+*.7z binary
+*.ttf binary
+
+# Documents
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
diff --git a/dashboard-project-app/.gitignore b/dashboard-project-app/.gitignore
new file mode 100755
index 0000000..914ada4
--- /dev/null
+++ b/dashboard-project-app/.gitignore
@@ -0,0 +1,14 @@
+node_modules
+public
+.tmp
+.idea
+client/bower_components
+dist
+/server/config/local.env.js
+npm-debug.log
+coverage
+.DS_Store
+/client/.DS_Store
+/client/app/.DS_Store
+/client/components/sdk/
+../.idea
diff --git a/dashboard-project-app/.jscsrc b/dashboard-project-app/.jscsrc
new file mode 100755
index 0000000..8923ad5
--- /dev/null
+++ b/dashboard-project-app/.jscsrc
@@ -0,0 +1,48 @@
+{
+ "excludeFiles": [
+ "client/app/app.constant.js"
+ ],
+ "esnext": true,
+ "maximumLineLength": {
+ "value": 100,
+ "allowComments": true,
+ "allowRegex": true
+ },
+ "disallowMixedSpacesAndTabs": true,
+ "disallowMultipleLineStrings": true,
+ "disallowNewlineBeforeBlockStatements": true,
+ "disallowSpaceAfterObjectKeys": true,
+ "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"],
+ "disallowSpaceBeforeBinaryOperators": [","],
+ "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"],
+ "disallowSpacesInAnonymousFunctionExpression": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowSpacesInFunctionDeclaration": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowSpacesInNamedFunctionExpression": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowSpacesInsideArrayBrackets": true,
+ "disallowSpacesInsideParentheses": true,
+ "disallowTrailingComma": true,
+ "disallowTrailingWhitespace": true,
+ "requireCommaBeforeLineBreak": true,
+ "requireLineFeedAtFileEnd": true,
+ "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"],
+ "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"],
+ "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"],
+ "requireSpaceBeforeBlockStatements": true,
+ "requireSpacesInConditionalExpression": {
+ "afterTest": true,
+ "beforeConsequent": true,
+ "afterConsequent": true,
+ "beforeAlternate": true
+ },
+ "requireSpacesInFunction": {
+ "beforeOpeningCurlyBrace": true
+ },
+ "validateLineBreaks": "LF",
+ "validateParameterSeparator": ", "
+}
diff --git a/dashboard-project-app/.travis.yml b/dashboard-project-app/.travis.yml
new file mode 100755
index 0000000..85b9616
--- /dev/null
+++ b/dashboard-project-app/.travis.yml
@@ -0,0 +1,11 @@
+language: node_js
+node_js:
+ - 4.2.3
+matrix:
+ fast_finish: true
+ allow_failures:
+ - node_js: 5.1.1
+before_script:
+ - npm install -g bower grunt-cli
+ - bower install
+services: mongodb
diff --git a/dashboard-project-app/.yo-rc.json b/dashboard-project-app/.yo-rc.json
new file mode 100755
index 0000000..6c782bf
--- /dev/null
+++ b/dashboard-project-app/.yo-rc.json
@@ -0,0 +1,67 @@
+{
+ "generator-angular-fullstack": {
+ "generatorVersion": "3.5.0",
+ "endpointDirectory": "server/api/",
+ "insertRoutes": true,
+ "registerRoutesFile": "server/routes.js",
+ "routesNeedle": "// Insert routes below",
+ "routesBase": "/api/",
+ "pluralizeRoutes": true,
+ "insertSockets": true,
+ "registerSocketsFile": "server/config/socketio.js",
+ "socketsNeedle": "// Insert sockets below",
+ "insertModels": true,
+ "registerModelsFile": "server/sqldb/index.js",
+ "modelsNeedle": "// Insert models below",
+ "filters": {
+ "js": true,
+ "babel": true,
+ "html": true,
+ "css": true,
+ "uirouter": true,
+ "bootstrap": true,
+ "uibootstrap": true,
+ "models": true,
+ "mongooseModels": true,
+ "mongoose": true,
+ "grunt": true,
+ "mocha": true,
+ "jasmine": false,
+ "should": false,
+ "expect": true
+ }
+ },
+ "generator-ng-component": {
+ "routeDirectory": "client/app/",
+ "directiveDirectory": "client/app/",
+ "componentDirectory": "app/components/",
+ "filterDirectory": "client/app/",
+ "serviceDirectory": "client/app/",
+ "basePath": "client",
+ "moduleName": "",
+ "modulePrompt": true,
+ "filters": [
+ "uirouter",
+ "mocha",
+ "expect",
+ "uirouter",
+ "es6"
+ ],
+ "extensions": [
+ "babel",
+ "js",
+ "html",
+ "css"
+ ],
+ "directiveSimpleTemplates": "",
+ "directiveComplexTemplates": "",
+ "filterTemplates": "",
+ "serviceTemplates": "",
+ "factoryTemplates": "",
+ "controllerTemplates": "",
+ "componentTemplates": "",
+ "decoratorTemplates": "",
+ "providerTemplates": "",
+ "routeTemplates": ""
+ }
+}
\ No newline at end of file
diff --git a/dashboard-project-app/Gruntfile.js b/dashboard-project-app/Gruntfile.js
new file mode 100755
index 0000000..4c8cf81
--- /dev/null
+++ b/dashboard-project-app/Gruntfile.js
@@ -0,0 +1,782 @@
+// Generated on 2016-03-29 using generator-angular-fullstack 3.5.0
+'use strict';
+
+module.exports = function (grunt) {
+ var localConfig;
+ try {
+ localConfig = require('./server/config/local.env');
+ } catch(e) {
+ localConfig = {};
+ }
+
+ // Load grunt tasks automatically, when needed
+ require('jit-grunt')(grunt, {
+ express: 'grunt-express-server',
+ useminPrepare: 'grunt-usemin',
+ ngtemplates: 'grunt-angular-templates',
+ cdnify: 'grunt-google-cdn',
+ protractor: 'grunt-protractor-runner',
+ buildcontrol: 'grunt-build-control',
+ istanbul_check_coverage: 'grunt-mocha-istanbul',
+ ngconstant: 'grunt-ng-constant'
+ });
+
+ // Time how long tasks take. Can help when optimizing build times
+ require('time-grunt')(grunt);
+
+ // Define the configuration for all the tasks
+ grunt.initConfig({
+
+ // Project settings
+ pkg: grunt.file.readJSON('package.json'),
+ yeoman: {
+ // configurable paths
+ client: require('./bower.json').appPath || 'client',
+ server: 'server',
+ dist: 'dist'
+ },
+ express: {
+ options: {
+ port: process.env.PORT || 9000
+ },
+ dev: {
+ options: {
+ script: '<%= yeoman.server %>',
+ debug: true
+ }
+ },
+ prod: {
+ options: {
+ script: '<%= yeoman.dist %>/<%= yeoman.server %>'
+ }
+ }
+ },
+ open: {
+ server: {
+ url: 'http://localhost:<%= express.options.port %>'
+ }
+ },
+ watch: {
+ babel: {
+ files: ['<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js'],
+ tasks: ['newer:babel:client']
+ },
+ ngconstant: {
+ files: ['<%= yeoman.server %>/config/environment/shared.js'],
+ tasks: ['ngconstant']
+ },
+ injectJS: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js',
+ '!<%= yeoman.client %>/app/app.js'
+ ],
+ tasks: ['injector:scripts']
+ },
+ injectCss: {
+ files: ['<%= yeoman.client %>/{app,components}/**/*.css'],
+ tasks: ['injector:css']
+ },
+ mochaTest: {
+ files: ['<%= yeoman.server %>/**/*.{spec,integration}.js'],
+ tasks: ['env:test', 'mochaTest']
+ },
+ jsTest: {
+ files: ['<%= yeoman.client %>/{app,components}/**/*.{spec,mock}.js'],
+ tasks: ['newer:jshint:all', 'wiredep:test', 'karma']
+ },
+ gruntfile: {
+ files: ['Gruntfile.js']
+ },
+ livereload: {
+ files: [
+ '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.{css,html}',
+ '{.tmp,<%= yeoman.client %>}/{app,components}/**/!(*.spec|*.mock).js',
+ '<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}'
+ ],
+ options: {
+ livereload: true
+ }
+ },
+ express: {
+ files: ['<%= yeoman.server %>/**/*.{js,json}'],
+ tasks: ['express:dev', 'wait'],
+ options: {
+ livereload: true,
+ spawn: false //Without this option specified express won't be reloaded
+ }
+ },
+ bower: {
+ files: ['bower.json'],
+ tasks: ['wiredep']
+ },
+ },
+
+ // Make sure code styles are up to par and there are no obvious mistakes
+ jshint: {
+ options: {
+ jshintrc: '<%= yeoman.client %>/.jshintrc',
+ reporter: require('jshint-stylish')
+ },
+ server: {
+ options: {
+ jshintrc: '<%= yeoman.server %>/.jshintrc'
+ },
+ src: ['<%= yeoman.server %>/**/!(*.spec|*.integration).js']
+ },
+ serverTest: {
+ options: {
+ jshintrc: '<%= yeoman.server %>/.jshintrc-spec'
+ },
+ src: ['<%= yeoman.server %>/**/*.{spec,integration}.js']
+ },
+ all: ['<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock|app.constant).js'],
+ test: {
+ src: ['<%= yeoman.client %>/{app,components}/**/*.{spec,mock}.js']
+ }
+ },
+
+ jscs: {
+ options: {
+ config: ".jscsrc"
+ },
+ main: {
+ files: {
+ src: [
+ '<%= yeoman.client %>/app/**/*.js',
+ '<%= yeoman.server %>/**/*.js'
+ ]
+ }
+ }
+ },
+
+ // Empties folders to start fresh
+ clean: {
+ dist: {
+ files: [{
+ dot: true,
+ src: [
+ '.tmp',
+ '<%= yeoman.dist %>/!(.git*|.openshift|Procfile)**'
+ ]
+ }]
+ },
+ server: '.tmp'
+ },
+
+ // Add vendor prefixed styles
+ postcss: {
+ options: {
+ map: true,
+ processors: [
+ require('autoprefixer')({browsers: ['last 2 version']})
+ ]
+ },
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '.tmp/',
+ src: '{,*/}*.css',
+ dest: '.tmp/'
+ }]
+ }
+ },
+
+ // Debugging with node inspector
+ 'node-inspector': {
+ custom: {
+ options: {
+ 'web-host': 'localhost'
+ }
+ }
+ },
+
+ // Use nodemon to run server in debug mode with an initial breakpoint
+ nodemon: {
+ debug: {
+ script: '<%= yeoman.server %>',
+ options: {
+ nodeArgs: ['--debug-brk'],
+ env: {
+ PORT: process.env.PORT || 9000
+ },
+ callback: function (nodemon) {
+ nodemon.on('log', function (event) {
+ console.log(event.colour);
+ });
+
+ // opens browser on initial server start
+ nodemon.on('config:update', function () {
+ setTimeout(function () {
+ require('open')('http://localhost:8080/debug?port=5858');
+ }, 500);
+ });
+ }
+ }
+ }
+ },
+
+ // Automatically inject Bower components into the app and karma.conf.js
+ wiredep: {
+ options: {
+ exclude: [
+ /bootstrap.js/,
+ '/json3/',
+ '/es5-shim/'
+ ]
+ },
+ client: {
+ src: '<%= yeoman.client %>/index.html',
+ ignorePath: '<%= yeoman.client %>/',
+ },
+ test: {
+ src: './karma.conf.js',
+ devDependencies: true
+ }
+ },
+
+ // Renames files for browser caching purposes
+ filerev: {
+ dist: {
+ src: [
+ '<%= yeoman.dist %>/<%= yeoman.client %>/!(bower_components){,*/}*.{js,css}',
+ '<%= yeoman.dist %>/<%= yeoman.client %>/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
+ ]
+ }
+ },
+
+ // Reads HTML for usemin blocks to enable smart builds that automatically
+ // concat, minify and revision files. Creates configurations in memory so
+ // additional tasks can operate on them
+ useminPrepare: {
+ html: ['<%= yeoman.client %>/index.html'],
+ options: {
+ dest: '<%= yeoman.dist %>/<%= yeoman.client %>'
+ }
+ },
+
+ // Performs rewrites based on rev and the useminPrepare configuration
+ usemin: {
+ html: ['<%= yeoman.dist %>/<%= yeoman.client %>/{,!(bower_components)/**/}*.html'],
+ css: ['<%= yeoman.dist %>/<%= yeoman.client %>/!(bower_components){,*/}*.css'],
+ js: ['<%= yeoman.dist %>/<%= yeoman.client %>/!(bower_components){,*/}*.js'],
+ options: {
+ assetsDirs: [
+ '<%= yeoman.dist %>/<%= yeoman.client %>',
+ '<%= yeoman.dist %>/<%= yeoman.client %>/assets/images'
+ ],
+ // This is so we update image references in our ng-templates
+ patterns: {
+ css: [
+ [/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the CSS to reference our revved images']
+ ],
+ js: [
+ [/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images']
+ ]
+ }
+ }
+ },
+
+ // The following *-min tasks produce minified files in the dist folder
+ imagemin: {
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '<%= yeoman.client %>/assets/images',
+ src: '{,*/}*.{png,jpg,jpeg,gif,svg}',
+ dest: '<%= yeoman.dist %>/<%= yeoman.client %>/assets/images'
+ }]
+ }
+ },
+
+ // Allow the use of non-minsafe AngularJS files. Automatically makes it
+ // minsafe compatible so Uglify does not destroy the ng references
+ ngAnnotate: {
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '.tmp/concat',
+ src: '**/*.js',
+ dest: '.tmp/concat'
+ }]
+ }
+ },
+
+ // Dynamically generate angular constant `appConfig` from
+ // `server/config/environment/shared.js`
+ ngconstant: {
+ options: {
+ name: 'dashboardProjectApp.constants',
+ dest: '<%= yeoman.client %>/app/app.constant.js',
+ deps: [],
+ wrap: true,
+ configPath: '<%= yeoman.server %>/config/environment/shared'
+ },
+ app: {
+ constants: function() {
+ return {
+ appConfig: require('./' + grunt.config.get('ngconstant.options.configPath'))
+ };
+ }
+ }
+ },
+
+ // Package all the html partials into a single javascript payload
+ ngtemplates: {
+ options: {
+ // This should be the name of your apps angular module
+ module: 'dashboardProjectApp',
+ htmlmin: {
+ collapseBooleanAttributes: true,
+ collapseWhitespace: true,
+ removeAttributeQuotes: true,
+ removeEmptyAttributes: true,
+ removeRedundantAttributes: true,
+ removeScriptTypeAttributes: true,
+ removeStyleLinkTypeAttributes: true
+ },
+ usemin: 'app/app.js'
+ },
+ main: {
+ cwd: '<%= yeoman.client %>',
+ src: ['{app,components}/**/*.html'],
+ dest: '.tmp/templates.js'
+ },
+ tmp: {
+ cwd: '.tmp',
+ src: ['{app,components}/**/*.html'],
+ dest: '.tmp/tmp-templates.js'
+ }
+ },
+
+ // Replace Google CDN references
+ cdnify: {
+ dist: {
+ html: ['<%= yeoman.dist %>/<%= yeoman.client %>/*.html']
+ }
+ },
+
+ // Copies remaining files to places other tasks can use
+ copy: {
+ dist: {
+ files: [{
+ expand: true,
+ dot: true,
+ cwd: '<%= yeoman.client %>',
+ dest: '<%= yeoman.dist %>/<%= yeoman.client %>',
+ src: [
+ '*.{ico,png,txt}',
+ '.htaccess',
+ 'bower_components/**/*',
+ 'assets/images/{,*/}*.{webp}',
+ 'assets/fonts/**/*',
+ 'index.html'
+ ]
+ }, {
+ expand: true,
+ cwd: '.tmp/images',
+ dest: '<%= yeoman.dist %>/<%= yeoman.client %>/assets/images',
+ src: ['generated/*']
+ }, {
+ expand: true,
+ dest: '<%= yeoman.dist %>',
+ src: [
+ 'package.json',
+ '<%= yeoman.server %>/**/*',
+ '!<%= yeoman.server %>/config/local.env.sample.js'
+ ]
+ }]
+ },
+ styles: {
+ expand: true,
+ cwd: '<%= yeoman.client %>',
+ dest: '.tmp/',
+ src: ['{app,components}/**/*.css']
+ }
+ },
+
+ buildcontrol: {
+ options: {
+ dir: '<%= yeoman.dist %>',
+ commit: true,
+ push: true,
+ connectCommits: false,
+ message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
+ },
+ heroku: {
+ options: {
+ remote: 'heroku',
+ branch: 'master'
+ }
+ },
+ openshift: {
+ options: {
+ remote: 'openshift',
+ branch: 'master'
+ }
+ }
+ },
+
+ // Run some tasks in parallel to speed up the build process
+ concurrent: {
+ pre: [
+ 'ngconstant'
+ ],
+ server: [
+ 'newer:babel:client',
+ ],
+ test: [
+ 'newer:babel:client',
+ ],
+ debug: {
+ tasks: [
+ 'nodemon',
+ 'node-inspector'
+ ],
+ options: {
+ logConcurrentOutput: true
+ }
+ },
+ dist: [
+ 'newer:babel:client',
+ 'imagemin'
+ ]
+ },
+
+ // Test settings
+ karma: {
+ unit: {
+ configFile: 'karma.conf.js',
+ singleRun: true
+ }
+ },
+
+ mochaTest: {
+ options: {
+ reporter: 'spec',
+ require: 'mocha.conf.js',
+ timeout: 5000 // set default mocha spec timeout
+ },
+ unit: {
+ src: ['<%= yeoman.server %>/**/*.spec.js']
+ },
+ integration: {
+ src: ['<%= yeoman.server %>/**/*.integration.js']
+ }
+ },
+
+ mocha_istanbul: {
+ unit: {
+ options: {
+ excludes: ['**/*.{spec,mock,integration}.js'],
+ reporter: 'spec',
+ require: ['mocha.conf.js'],
+ mask: '**/*.spec.js',
+ coverageFolder: 'coverage/server/unit'
+ },
+ src: '<%= yeoman.server %>'
+ },
+ integration: {
+ options: {
+ excludes: ['**/*.{spec,mock,integration}.js'],
+ reporter: 'spec',
+ require: ['mocha.conf.js'],
+ mask: '**/*.integration.js',
+ coverageFolder: 'coverage/server/integration'
+ },
+ src: '<%= yeoman.server %>'
+ }
+ },
+
+ istanbul_check_coverage: {
+ default: {
+ options: {
+ coverageFolder: 'coverage/**',
+ check: {
+ lines: 80,
+ statements: 80,
+ branches: 80,
+ functions: 80
+ }
+ }
+ }
+ },
+
+ protractor: {
+ options: {
+ configFile: 'protractor.conf.js'
+ },
+ chrome: {
+ options: {
+ args: {
+ browser: 'chrome'
+ }
+ }
+ }
+ },
+
+ env: {
+ test: {
+ NODE_ENV: 'test'
+ },
+ prod: {
+ NODE_ENV: 'production'
+ },
+ all: localConfig
+ },
+
+ // Compiles ES6 to JavaScript using Babel
+ babel: {
+ options: {
+ sourceMap: true
+ },
+ client: {
+ files: [{
+ expand: true,
+ cwd: '<%= yeoman.client %>',
+ src: ['{app,components}/**/!(*.spec).js'],
+ dest: '.tmp'
+ }]
+ },
+ server: {
+ options: {
+ plugins: [
+ 'transform-class-properties',
+ 'transform-runtime'
+ ]
+ },
+ files: [{
+ expand: true,
+ cwd: '<%= yeoman.server %>',
+ src: [
+ '**/*.js',
+ '!config/local.env.sample.js'
+ ],
+ dest: '<%= yeoman.dist %>/<%= yeoman.server %>'
+ }]
+ }
+ },
+
+ injector: {
+ options: {},
+ // Inject application script files into index.html (doesn't include bower)
+ scripts: {
+ options: {
+ transform: function(filePath) {
+ var yoClient = grunt.config.get('yeoman.client');
+ filePath = filePath.replace('/' + yoClient + '/', '');
+ filePath = filePath.replace('/.tmp/', '');
+ return '';
+ },
+ sort: function(a, b) {
+ var module = /\.module\.(js|ts)$/;
+ var aMod = module.test(a);
+ var bMod = module.test(b);
+ // inject *.module.js first
+ return (aMod === bMod) ? 0 : (aMod ? -1 : 1);
+ },
+ starttag: '',
+ endtag: ''
+ },
+ files: {
+ '<%= yeoman.client %>/index.html': [
+ [
+ '<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js',
+ '!{.tmp,<%= yeoman.client %>}/app/app.{js,ts}'
+ ]
+ ]
+ }
+ },
+
+ // Inject component css into index.html
+ css: {
+ options: {
+ transform: function(filePath) {
+ var yoClient = grunt.config.get('yeoman.client');
+ filePath = filePath.replace('/' + yoClient + '/', '');
+ filePath = filePath.replace('/.tmp/', '');
+ return ' ';
+ },
+ starttag: '',
+ endtag: ''
+ },
+ files: {
+ '<%= yeoman.client %>/index.html': [
+ '<%= yeoman.client %>/{app,components}/**/*.css'
+ ]
+ }
+ }
+ },
+ });
+
+ // Used for delaying livereload until after server has restarted
+ grunt.registerTask('wait', function () {
+ grunt.log.ok('Waiting for server reload...');
+
+ var done = this.async();
+
+ setTimeout(function () {
+ grunt.log.writeln('Done waiting!');
+ done();
+ }, 1500);
+ });
+
+ grunt.registerTask('express-keepalive', 'Keep grunt running', function() {
+ this.async();
+ });
+
+ grunt.registerTask('serve', function (target) {
+ if (target === 'dist') {
+ return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'open', 'express-keepalive']);
+ }
+
+ if (target === 'debug') {
+ return grunt.task.run([
+ // 'clean:server',
+ 'env:all',
+ 'concurrent:pre',
+ 'concurrent:server',
+ 'injector',
+ 'wiredep:client',
+ 'postcss',
+ 'concurrent:debug'
+ ]);
+ }
+
+ grunt.task.run([
+ // 'clean:server',
+ 'env:all',
+ 'concurrent:pre',
+ 'concurrent:server',
+ 'injector',
+ 'wiredep:client',
+ 'postcss',
+ 'express:dev',
+ 'wait',
+ 'open',
+ 'watch'
+ ]);
+ });
+
+ grunt.registerTask('server', function () {
+ grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
+ grunt.task.run(['serve']);
+ });
+
+ grunt.registerTask('test', function(target, option) {
+ if (target === 'server') {
+ return grunt.task.run([
+ 'env:all',
+ 'env:test',
+ 'mochaTest:unit',
+ 'mochaTest:integration'
+ ]);
+ }
+
+ else if (target === 'client') {
+ return grunt.task.run([
+ // 'clean:server',
+ 'env:all',
+ 'concurrent:pre',
+ 'concurrent:test',
+ 'injector',
+ 'postcss',
+ 'wiredep:test',
+ 'karma'
+ ]);
+ }
+
+ else if (target === 'e2e') {
+
+ if (option === 'prod') {
+ return grunt.task.run([
+ 'build',
+ 'env:all',
+ 'env:prod',
+ 'express:prod',
+ 'protractor'
+ ]);
+ }
+
+ else {
+ return grunt.task.run([
+ // 'clean:server',
+ 'env:all',
+ 'env:test',
+ 'concurrent:pre',
+ 'concurrent:test',
+ 'injector',
+ 'wiredep:client',
+ 'postcss',
+ 'express:dev',
+ 'protractor'
+ ]);
+ }
+ }
+
+ else if (target === 'coverage') {
+
+ if (option === 'unit') {
+ return grunt.task.run([
+ 'env:all',
+ 'env:test',
+ 'mocha_istanbul:unit'
+ ]);
+ }
+
+ else if (option === 'integration') {
+ return grunt.task.run([
+ 'env:all',
+ 'env:test',
+ 'mocha_istanbul:integration'
+ ]);
+ }
+
+ else if (option === 'check') {
+ return grunt.task.run([
+ 'istanbul_check_coverage'
+ ]);
+ }
+
+ else {
+ return grunt.task.run([
+ 'env:all',
+ 'env:test',
+ 'mocha_istanbul',
+ 'istanbul_check_coverage'
+ ]);
+ }
+
+ }
+
+ else grunt.task.run([
+ 'test:server',
+ 'test:client'
+ ]);
+ });
+
+ grunt.registerTask('build', [
+ // 'clean:dist',
+ 'concurrent:pre',
+ 'concurrent:dist',
+ 'injector',
+ 'wiredep:client',
+ 'useminPrepare',
+ 'postcss',
+ 'ngtemplates',
+ 'concat',
+ 'ngAnnotate',
+ 'copy:dist',
+ 'babel:server',
+ 'cdnify',
+ 'cssmin',
+ 'uglify',
+ 'filerev',
+ 'usemin'
+ ]);
+
+ grunt.registerTask('default', [
+ 'newer:jshint',
+ 'test',
+ 'build'
+ ]);
+};
diff --git a/dashboard-project-app/README.md b/dashboard-project-app/README.md
new file mode 100755
index 0000000..51dd054
--- /dev/null
+++ b/dashboard-project-app/README.md
@@ -0,0 +1,31 @@
+# dashboard-project
+
+This project was generated with the [Angular Full-Stack Generator](https://github.com/DaftMonk/generator-angular-fullstack) version 3.5.0.
+
+## Getting Started
+
+### Prerequisites
+
+- [Git](https://git-scm.com/)
+- [Node.js and npm](nodejs.org) Node ^4.2.3, npm ^2.14.7
+- [Bower](bower.io) (`npm install --global bower`)
+- [Grunt](http://gruntjs.com/) (`npm install --global grunt-cli`)
+- [MongoDB](https://www.mongodb.org/) - Keep a running daemon with `mongod`
+
+### Developing
+
+1. Run `npm install` to install server dependencies.
+
+2. Run `bower install` to install front-end dependencies.
+
+3. Run `mongod` in a separate shell to keep an instance of the MongoDB Daemon running
+
+4. Run `grunt serve` to start the development server. It should automatically open the client in your browser when ready.
+
+## Build & development
+
+Run `grunt build` for building and `grunt serve` for preview.
+
+## Testing
+
+Running `npm test` will run the unit tests with karma.
diff --git a/dashboard-project-app/bower.json b/dashboard-project-app/bower.json
new file mode 100755
index 0000000..72507af
--- /dev/null
+++ b/dashboard-project-app/bower.json
@@ -0,0 +1,22 @@
+{
+ "name": "dashboard-project",
+ "version": "0.0.0",
+ "dependencies": {
+ "angular": "~1.5.0",
+ "json3": "~3.3.1",
+ "es5-shim": "~3.0.1",
+ "bootstrap": "~3.1.1",
+ "angular-resource": "~1.5.0",
+ "angular-cookies": "~1.5.0",
+ "angular-sanitize": "~1.5.0",
+ "angular-bootstrap": "~1.1.2",
+ "font-awesome": ">=4.1.0",
+ "lodash": "~2.4.1",
+ "angular-ui-router": "~0.2.15",
+ "bootstrap-table": "^1.10.1",
+ "moment": "^2.13.0"
+ },
+ "devDependencies": {
+ "angular-mocks": "~1.5.0"
+ }
+}
diff --git a/dashboard-project-app/build-sdk.sh b/dashboard-project-app/build-sdk.sh
new file mode 100755
index 0000000..182f5b6
--- /dev/null
+++ b/dashboard-project-app/build-sdk.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+mkdir client/components/sdk
+cp ../dashboard-project-api/js/lbServices.js client/components/sdk/
+grunt serve
diff --git a/dashboard-project-app/client/.htaccess b/dashboard-project-app/client/.htaccess
new file mode 100755
index 0000000..cb84cb9
--- /dev/null
+++ b/dashboard-project-app/client/.htaccess
@@ -0,0 +1,543 @@
+# Apache Configuration File
+
+# (!) Using `.htaccess` files slows down Apache, therefore, if you have access
+# to the main server config file (usually called `httpd.conf`), you should add
+# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html.
+
+# ##############################################################################
+# # CROSS-ORIGIN RESOURCE SHARING (CORS) #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Cross-domain AJAX requests |
+# ------------------------------------------------------------------------------
+
+# Enable cross-origin AJAX requests.
+# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
+# http://enable-cors.org/
+
+#
+# Header set Access-Control-Allow-Origin "*"
+#
+
+# ------------------------------------------------------------------------------
+# | CORS-enabled images |
+# ------------------------------------------------------------------------------
+
+# Send the CORS header for images when browsers request it.
+# https://developer.mozilla.org/en/CORS_Enabled_Image
+# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
+# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
+
+
+
+
+ SetEnvIf Origin ":" IS_CORS
+ Header set Access-Control-Allow-Origin "*" env=IS_CORS
+
+
+
+
+# ------------------------------------------------------------------------------
+# | Web fonts access |
+# ------------------------------------------------------------------------------
+
+# Allow access from all domains for web fonts
+
+
+
+ Header set Access-Control-Allow-Origin "*"
+
+
+
+
+# ##############################################################################
+# # ERRORS #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | 404 error prevention for non-existing redirected folders |
+# ------------------------------------------------------------------------------
+
+# Prevent Apache from returning a 404 error for a rewrite if a directory
+# with the same name does not exist.
+# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews
+# http://www.webmasterworld.com/apache/3808792.htm
+
+Options -MultiViews
+
+# ------------------------------------------------------------------------------
+# | Custom error messages / pages |
+# ------------------------------------------------------------------------------
+
+# You can customize what Apache returns to the client in case of an error (see
+# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.:
+
+ErrorDocument 404 /404.html
+
+
+# ##############################################################################
+# # INTERNET EXPLORER #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Better website experience |
+# ------------------------------------------------------------------------------
+
+# Force IE to render pages in the highest available mode in the various
+# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf.
+
+
+ Header set X-UA-Compatible "IE=edge"
+ # `mod_headers` can't match based on the content-type, however, we only
+ # want to send this header for HTML pages and not for the other resources
+
+ Header unset X-UA-Compatible
+
+
+
+# ------------------------------------------------------------------------------
+# | Cookie setting from iframes |
+# ------------------------------------------------------------------------------
+
+# Allow cookies to be set from iframes in IE.
+
+#
+# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
+#
+
+# ------------------------------------------------------------------------------
+# | Screen flicker |
+# ------------------------------------------------------------------------------
+
+# Stop screen flicker in IE on CSS rollovers (this only works in
+# combination with the `ExpiresByType` directives for images from below).
+
+# BrowserMatch "MSIE" brokenvary=1
+# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1
+# BrowserMatch "Opera" !brokenvary
+# SetEnvIf brokenvary 1 force-no-vary
+
+
+# ##############################################################################
+# # MIME TYPES AND ENCODING #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Proper MIME types for all files |
+# ------------------------------------------------------------------------------
+
+
+
+ # Audio
+ AddType audio/mp4 m4a f4a f4b
+ AddType audio/ogg oga ogg
+
+ # JavaScript
+ # Normalize to standard type (it's sniffed in IE anyways):
+ # http://tools.ietf.org/html/rfc4329#section-7.2
+ AddType application/javascript js jsonp
+ AddType application/json json
+
+ # Video
+ AddType video/mp4 mp4 m4v f4v f4p
+ AddType video/ogg ogv
+ AddType video/webm webm
+ AddType video/x-flv flv
+
+ # Web fonts
+ AddType application/font-woff woff
+ AddType application/vnd.ms-fontobject eot
+
+ # Browsers usually ignore the font MIME types and sniff the content,
+ # however, Chrome shows a warning if other MIME types are used for the
+ # following fonts.
+ AddType application/x-font-ttf ttc ttf
+ AddType font/opentype otf
+
+ # Make SVGZ fonts work on iPad:
+ # https://twitter.com/FontSquirrel/status/14855840545
+ AddType image/svg+xml svg svgz
+ AddEncoding gzip svgz
+
+ # Other
+ AddType application/octet-stream safariextz
+ AddType application/x-chrome-extension crx
+ AddType application/x-opera-extension oex
+ AddType application/x-shockwave-flash swf
+ AddType application/x-web-app-manifest+json webapp
+ AddType application/x-xpinstall xpi
+ AddType application/xml atom rdf rss xml
+ AddType image/webp webp
+ AddType image/x-icon ico
+ AddType text/cache-manifest appcache manifest
+ AddType text/vtt vtt
+ AddType text/x-component htc
+ AddType text/x-vcard vcf
+
+
+
+# ------------------------------------------------------------------------------
+# | UTF-8 encoding |
+# ------------------------------------------------------------------------------
+
+# Use UTF-8 encoding for anything served as `text/html` or `text/plain`.
+AddDefaultCharset utf-8
+
+# Force UTF-8 for certain file formats.
+
+ AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml
+
+
+
+# ##############################################################################
+# # URL REWRITES #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Rewrite engine |
+# ------------------------------------------------------------------------------
+
+# Turning on the rewrite engine and enabling the `FollowSymLinks` option is
+# necessary for the following directives to work.
+
+# If your web host doesn't allow the `FollowSymlinks` option, you may need to
+# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the
+# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
+
+# Also, some cloud hosting services require `RewriteBase` to be set:
+# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site
+
+
+ Options +FollowSymlinks
+ # Options +SymLinksIfOwnerMatch
+ RewriteEngine On
+ # RewriteBase /
+
+
+# ------------------------------------------------------------------------------
+# | Suppressing / Forcing the "www." at the beginning of URLs |
+# ------------------------------------------------------------------------------
+
+# The same content should never be available under two different URLs especially
+# not with and without "www." at the beginning. This can cause SEO problems
+# (duplicate content), therefore, you should choose one of the alternatives and
+# redirect the other one.
+
+# By default option 1 (no "www.") is activated:
+# http://no-www.org/faq.php?q=class_b
+
+# If you'd prefer to use option 2, just comment out all the lines from option 1
+# and uncomment the ones from option 2.
+
+# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME!
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Option 1: rewrite www.example.com → example.com
+
+
+ RewriteCond %{HTTPS} !=on
+ RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
+ RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Option 2: rewrite example.com → www.example.com
+
+# Be aware that the following might not be a good idea if you use "real"
+# subdomains for certain parts of your website.
+
+#
+# RewriteCond %{HTTPS} !=on
+# RewriteCond %{HTTP_HOST} !^www\..+$ [NC]
+# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
+#
+
+
+# ##############################################################################
+# # SECURITY #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Content Security Policy (CSP) |
+# ------------------------------------------------------------------------------
+
+# You can mitigate the risk of cross-site scripting and other content-injection
+# attacks by setting a Content Security Policy which whitelists trusted sources
+# of content for your site.
+
+# The example header below allows ONLY scripts that are loaded from the current
+# site's origin (no inline scripts, no CDN, etc). This almost certainly won't
+# work as-is for your site!
+
+# To get all the details you'll need to craft a reasonable policy for your site,
+# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or
+# see the specification: http://w3.org/TR/CSP).
+
+#
+# Header set Content-Security-Policy "script-src 'self'; object-src 'self'"
+#
+# Header unset Content-Security-Policy
+#
+#
+
+# ------------------------------------------------------------------------------
+# | File access |
+# ------------------------------------------------------------------------------
+
+# Block access to directories without a default document.
+# Usually you should leave this uncommented because you shouldn't allow anyone
+# to surf through every directory on your server (which may includes rather
+# private places like the CMS's directories).
+
+
+ Options -Indexes
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Block access to hidden files and directories.
+# This includes directories used by version control systems such as Git and SVN.
+
+
+ RewriteCond %{SCRIPT_FILENAME} -d [OR]
+ RewriteCond %{SCRIPT_FILENAME} -f
+ RewriteRule "(^|/)\." - [F]
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Block access to backup and source files.
+# These files may be left by some text editors and can pose a great security
+# danger when anyone has access to them.
+
+
+ Order allow,deny
+ Deny from all
+ Satisfy All
+
+
+# ------------------------------------------------------------------------------
+# | Secure Sockets Layer (SSL) |
+# ------------------------------------------------------------------------------
+
+# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.:
+# prevent `https://www.example.com` when your certificate only allows
+# `https://secure.example.com`.
+
+#
+# RewriteCond %{SERVER_PORT} !^443
+# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L]
+#
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Force client-side SSL redirection.
+
+# If a user types "example.com" in his browser, the above rule will redirect him
+# to the secure version of the site. That still leaves a window of opportunity
+# (the initial HTTP connection) for an attacker to downgrade or redirect the
+# request. The following header ensures that browser will ONLY connect to your
+# server via HTTPS, regardless of what the users type in the address bar.
+# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/
+
+#
+# Header set Strict-Transport-Security max-age=16070400;
+#
+
+# ------------------------------------------------------------------------------
+# | Server software information |
+# ------------------------------------------------------------------------------
+
+# Avoid displaying the exact Apache version number, the description of the
+# generic OS-type and the information about Apache's compiled-in modules.
+
+# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`!
+
+# ServerTokens Prod
+
+
+# ##############################################################################
+# # WEB PERFORMANCE #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Compression |
+# ------------------------------------------------------------------------------
+
+
+
+ # Force compression for mangled headers.
+ # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
+
+
+ SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
+ RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
+
+
+
+ # Compress all output labeled with one of the following MIME-types
+ # (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
+ # and can remove the `` and ` ` lines
+ # as `AddOutputFilterByType` is still in the core directives).
+
+ AddOutputFilterByType DEFLATE application/atom+xml \
+ application/javascript \
+ application/json \
+ application/rss+xml \
+ application/vnd.ms-fontobject \
+ application/x-font-ttf \
+ application/x-web-app-manifest+json \
+ application/xhtml+xml \
+ application/xml \
+ font/opentype \
+ image/svg+xml \
+ image/x-icon \
+ text/css \
+ text/html \
+ text/plain \
+ text/x-component \
+ text/xml
+
+
+
+
+# ------------------------------------------------------------------------------
+# | Content transformations |
+# ------------------------------------------------------------------------------
+
+# Prevent some of the mobile network providers from modifying the content of
+# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5.
+
+#
+# Header set Cache-Control "no-transform"
+#
+
+# ------------------------------------------------------------------------------
+# | ETag removal |
+# ------------------------------------------------------------------------------
+
+# Since we're sending far-future expires headers (see below), ETags can
+# be removed: http://developer.yahoo.com/performance/rules.html#etags.
+
+# `FileETag None` is not enough for every server.
+
+ Header unset ETag
+
+
+FileETag None
+
+# ------------------------------------------------------------------------------
+# | Expires headers (for better cache control) |
+# ------------------------------------------------------------------------------
+
+# The following expires headers are set pretty far in the future. If you don't
+# control versioning with filename-based cache busting, consider lowering the
+# cache time for resources like CSS and JS to something like 1 week.
+
+
+
+ ExpiresActive on
+ ExpiresDefault "access plus 1 month"
+
+ # CSS
+ ExpiresByType text/css "access plus 1 year"
+
+ # Data interchange
+ ExpiresByType application/json "access plus 0 seconds"
+ ExpiresByType application/xml "access plus 0 seconds"
+ ExpiresByType text/xml "access plus 0 seconds"
+
+ # Favicon (cannot be renamed!)
+ ExpiresByType image/x-icon "access plus 1 week"
+
+ # HTML components (HTCs)
+ ExpiresByType text/x-component "access plus 1 month"
+
+ # HTML
+ ExpiresByType text/html "access plus 0 seconds"
+
+ # JavaScript
+ ExpiresByType application/javascript "access plus 1 year"
+
+ # Manifest files
+ ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
+ ExpiresByType text/cache-manifest "access plus 0 seconds"
+
+ # Media
+ ExpiresByType audio/ogg "access plus 1 month"
+ ExpiresByType image/gif "access plus 1 month"
+ ExpiresByType image/jpeg "access plus 1 month"
+ ExpiresByType image/png "access plus 1 month"
+ ExpiresByType video/mp4 "access plus 1 month"
+ ExpiresByType video/ogg "access plus 1 month"
+ ExpiresByType video/webm "access plus 1 month"
+
+ # Web feeds
+ ExpiresByType application/atom+xml "access plus 1 hour"
+ ExpiresByType application/rss+xml "access plus 1 hour"
+
+ # Web fonts
+ ExpiresByType application/font-woff "access plus 1 month"
+ ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
+ ExpiresByType application/x-font-ttf "access plus 1 month"
+ ExpiresByType font/opentype "access plus 1 month"
+ ExpiresByType image/svg+xml "access plus 1 month"
+
+
+
+# ------------------------------------------------------------------------------
+# | Filename-based cache busting |
+# ------------------------------------------------------------------------------
+
+# If you're not using a build process to manage your filename version revving,
+# you might want to consider enabling the following directives to route all
+# requests such as `/css/style.12345.css` to `/css/style.css`.
+
+# To understand why this is important and a better idea than `*.css?v231`, read:
+# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring
+
+#
+# RewriteCond %{REQUEST_FILENAME} !-f
+# RewriteCond %{REQUEST_FILENAME} !-d
+# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
+#
+
+# ------------------------------------------------------------------------------
+# | File concatenation |
+# ------------------------------------------------------------------------------
+
+# Allow concatenation from within specific CSS and JS files, e.g.:
+# Inside of `script.combined.js` you could have
+#
+#
+# and they would be included into this single file.
+
+#
+#
+# Options +Includes
+# AddOutputFilterByType INCLUDES application/javascript application/json
+# SetOutputFilter INCLUDES
+#
+#
+# Options +Includes
+# AddOutputFilterByType INCLUDES text/css
+# SetOutputFilter INCLUDES
+#
+#
+
+# ------------------------------------------------------------------------------
+# | Persistent connections |
+# ------------------------------------------------------------------------------
+
+# Allow multiple requests to be sent over the same TCP connection:
+# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive.
+
+# Enable if you serve a lot of static content but, be aware of the
+# possible disadvantages!
+
+#
+# Header set Connection Keep-Alive
+#
diff --git a/dashboard-project-app/client/.jshintrc b/dashboard-project-app/client/.jshintrc
new file mode 100755
index 0000000..6d5c2ca
--- /dev/null
+++ b/dashboard-project-app/client/.jshintrc
@@ -0,0 +1,39 @@
+{
+ "node": true,
+ "browser": true,
+ "esnext": true,
+ "bitwise": true,
+ "camelcase": true,
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "quotmark": "single",
+ "undef": true,
+ "unused": true,
+ "strict": true,
+ "trailing": true,
+ "smarttabs": true,
+ "ignoreDelimiters": [
+ { "start": "start-non-standard", "end": "end-non-standard" }
+ ],
+ "globals": {
+ "jQuery": true,
+ "angular": true,
+ "console": true,
+ "$": true,
+ "_": true,
+ "moment": true,
+ "describe": true,
+ "beforeEach": true,
+ "module": true,
+ "inject": true,
+ "it": true,
+ "expect": true,
+ "browser": true,
+ "element": true,
+ "by": true
+ }
+}
diff --git a/dashboard-project-app/client/app/app.constant.js b/dashboard-project-app/client/app/app.constant.js
new file mode 100755
index 0000000..b239b30
--- /dev/null
+++ b/dashboard-project-app/client/app/app.constant.js
@@ -0,0 +1,9 @@
+(function(angular, undefined) {
+'use strict';
+
+angular.module('dashboardProjectApp.constants', [])
+
+.constant('appConfig', {userRoles:['guest','user','admin']})
+
+;
+})(angular);
\ No newline at end of file
diff --git a/dashboard-project-app/client/app/app.css b/dashboard-project-app/client/app/app.css
new file mode 100755
index 0000000..7a97efc
--- /dev/null
+++ b/dashboard-project-app/client/app/app.css
@@ -0,0 +1,135 @@
+
+/**
+ * Bootstrap Fonts
+ */
+
+@font-face {
+ font-family: 'Glyphicons Halflings';
+ src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot');
+ src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
+ url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'),
+ url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
+ url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
+}
+
+/**
+ *Font Awesome Fonts
+ */
+
+@font-face {
+ font-family: 'FontAwesome';
+ src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0');
+ src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'),
+ url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'),
+ url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'),
+ url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+/**
+* General font
+**/
+@font-face {
+ font-family:'OpenSans-Regular';
+ src: url('../assets/fonts/open-sans/OpenSans-Regular.ttf') format('truetype');
+}
+@font-face {
+ font-family:'OpenSans-Bold';
+ src: url('../assets/fonts/open-sans/OpenSans-Bold.ttf') format('truetype');
+}
+@font-face {
+ font-family:'OpenSans-Light';
+ src: url('../assets/fonts/open-sans/OpenSans-Light.ttf') format('truetype');
+}
+/**
+ * App-wide Styles
+ */
+
+.browserupgrade {
+ margin: 0.2em 0;
+ background: #ccc;
+ color: #000;
+ padding: 0.2em 0;
+}
+body {
+ font-family:"OpenSans-Regular", Helvetica, Arial, sans-serif;
+}
+header {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ padding-left: 0px;
+ border-bottom: 2px solid #e1e1e1;
+}
+
+h3 {
+ font-family: "OpenSans-Bold", Helvetica, Arial, sans-serif;
+ color: #2A4E68;
+}
+
+.dp-title {
+ font-family: "OpenSans-Bold", Helvetica, Arial, sans-serif;
+ color: #2A4E68;
+ font-size: 24px;
+}
+.dp-label{
+ font-size: 12px;
+ top: -5px;
+ position: relative;
+}
+
+.dp-label-completed{
+ background-color: #58B957;
+}
+.dp-label-in-progress{
+ background-color: #0D5B9D;
+}
+.dp-label-none{
+ background-color: #999999;
+}
+.dp-label-pending{
+ background-color: #EFAC43;
+}
+
+.dp-link-title {
+ color:#2E729A;
+}
+.dp-link-title:hover {
+ font-weight: bold;
+ text-decoration: none;
+}
+
+.add-margin-top{
+ margin-top: 10px;
+}
+.dp-hr{
+ margin-bottom: 5px;
+}
+
+.dp-subtitle{
+ font-weight: bolder;
+}
+
+.dp-nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus{
+ background-color: #0D5B9D;
+}
+
+.progress-bar .sr-only{
+ position: relative;
+}
+
+.progress-bar[aria-valuenow="0"] {
+ color: #000;
+ min-width: 100%;
+ background: transparent;
+ box-shadow: none;
+}
+
+.compress-text{
+ max-height: 63px;
+ overflow: hidden;
+}
+
+.uncompress-text{
+ max-height: none !important;
+}
diff --git a/dashboard-project-app/client/app/app.js b/dashboard-project-app/client/app/app.js
new file mode 100755
index 0000000..99cb27f
--- /dev/null
+++ b/dashboard-project-app/client/app/app.js
@@ -0,0 +1,20 @@
+'use strict';
+
+angular.module('dashboardProjectApp', [
+ 'dashboardProjectApp.constants',
+ 'ngCookies',
+ 'ngResource',
+ 'ngSanitize',
+ 'ui.router',
+ 'ui.bootstrap',
+ 'lbServices'
+])
+ .config(function($urlRouterProvider, $locationProvider, LoopBackResourceProvider) {
+ LoopBackResourceProvider.setUrlBase('http://localhost:3004/api');
+ //$urlRouterProvider.otherwise('/');
+
+ $locationProvider.html5Mode(true);
+ })
+ .run(function($rootScope, $state) {
+ $state.go("projectList")
+ });
diff --git a/dashboard-project-app/client/app/projectDetail/controllers/projectDetail.controller.js b/dashboard-project-app/client/app/projectDetail/controllers/projectDetail.controller.js
new file mode 100755
index 0000000..48ab848
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/controllers/projectDetail.controller.js
@@ -0,0 +1,89 @@
+'use strict';
+(function(){
+
+ angular.module('dashboardProjectApp')
+ .controller('projectDetailController', ['$scope','$state', 'UserStory'/*, 'tasksService', 'tasks', 'task'*/,
+ function($scope, $state, UserStory/*, tasksService, tasks, task*/) {
+
+ $scope.taskId = $state.params.id;
+ $scope.openTasks = {};
+ $scope.showText = {}
+
+ $scope.closeTask = function(task){
+ $scope.openTasks[task] = !$scope.openTasks[task];
+ }
+
+ $scope.getProjectIcon = function(key){
+
+ if(key == 'nova')
+ return 'fa fa-cogs';
+
+ if(key == 'cinder')
+ return 'fa fa-archive'
+
+ if(key == 'keystone')
+ return 'fa fa-key';
+
+ if(key == 'swift')
+ return 'fa fa-object-group';
+
+ if(key == 'neutron')
+ return 'fa fa-cloud-upload';
+
+ if(key == 'glance')
+ return 'fa fa-key';
+
+ return 'fa fa-cog';
+ }
+
+ $scope.actualProject = {};
+
+ function getFile() {
+ UserStory.findById({id:$scope.taskId},
+ function success(userStory) {
+ $scope.userStory = userStory;
+
+ $scope.userStory.updatedOn = moment($scope.userStory.updatedOn).format("MM-DD-YYYY");
+ for(var key in $scope.userStory.tasks_status) {
+ $scope.actualProject[key] = $scope.userStory.tasks_status[key].projects[0]
+ }
+
+ });
+ };
+
+ $scope.selectProject = function(keyProject, idTask){
+ $scope.actualProject[idTask] = keyProject
+ }
+
+ getFile();
+
+ $scope.showMore = function(key){
+ $scope.showText[key] = true;
+ }
+
+ $scope.showLess = function(key){
+ $scope.showText[key] = false;
+ }
+
+ $scope.mailTo = function(user, email){
+ window.location.href = "mailto:" + email + "?subject=Mail to " + email;
+ }
+
+
+ }])
+ .filter('capitalize', function() {
+ return function(input) {
+ return (!!input) ? input.charAt(0).toUpperCase() + input.substr(1).toLowerCase() : '';
+ }
+ }).filter('removeDashes', function() {
+ return function(string) {
+
+ if (!angular.isString(string)) {
+ return string;
+ }
+
+ return string.replace(/-/g, ' ');
+ };
+ })
+
+})();
diff --git a/dashboard-project-app/client/app/projectDetail/projectDetail.js b/dashboard-project-app/client/app/projectDetail/projectDetail.js
new file mode 100755
index 0000000..eb6dfac
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/projectDetail.js
@@ -0,0 +1,19 @@
+'use strict';
+
+angular.module('dashboardProjectApp')
+ .config(function ($stateProvider) {
+
+ $stateProvider
+ .state('projectDetail', {
+ url: '/projectDetail/:id',
+ /* resolve: {
+ userStory: ['userStoryService',
+ function(userStoryService) {
+ return userStoryService.getTasks();
+ }]
+ },*/
+ templateUrl: 'app/projectDetail/views/projectDetail.html',
+ controller: 'projectDetailController'
+ })
+
+ });
diff --git a/dashboard-project-app/client/app/projectDetail/services/userStoryService.js b/dashboard-project-app/client/app/projectDetail/services/userStoryService.js
new file mode 100755
index 0000000..8e9b147
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/services/userStoryService.js
@@ -0,0 +1,18 @@
+angular.module('dashboardProjectApp')
+ .service('UserStoryService', ['$http','UserStory', function($http, UserStory) {
+
+ this.searchTask = function(query) {
+ //return $http.get('/tasks/search/' + query);
+ };
+
+ $scope.getUserStory = function getFiles() {
+
+ console.log('aaaaaaa')
+ return UserStory.findById({id:$scope.taskId});
+ };
+
+ this.getTask = function(name) {
+ //return $http.get('/tasks/' + name);
+ return {};
+ };
+ }]);
diff --git a/dashboard-project-app/client/app/projectDetail/styles/projectDetail.css b/dashboard-project-app/client/app/projectDetail/styles/projectDetail.css
new file mode 100755
index 0000000..7099c66
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/styles/projectDetail.css
@@ -0,0 +1,44 @@
+
+.frm-well{
+ border: 0px;
+ border-radius: 0px;
+ box-shadow: none;
+}
+
+.tasks{
+ background-color: #EDF2F7;
+ padding: 20px;
+}
+
+.quick-resume{
+ margin-top: 20px;
+}
+
+.progress{
+ background-color: #DEDEDE;
+}
+
+.dp-panel{
+ border-top: 6px solid #DA6359;
+}
+
+.task-open-icon{
+ font-size: 30px;
+ top: 4px;
+ position: relative;
+
+ -ms-transform: rotate(-90deg);
+ -webkit-transform: rotate(-90deg);
+ transform: rotate(-90deg);
+}
+
+.task-open-icon.down{
+ -ms-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ top: 0px;
+}
+
+
+table tr.separator { height: 30px; }
+
diff --git a/dashboard-project-app/client/app/projectDetail/tests/projectDetail.controller.spec.js b/dashboard-project-app/client/app/projectDetail/tests/projectDetail.controller.spec.js
new file mode 100755
index 0000000..1401a97
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/tests/projectDetail.controller.spec.js
@@ -0,0 +1,21 @@
+'use strict';
+
+describe('Component: ProjectDetailComponent', function () {
+
+ // load the controller's module
+ beforeEach(module('dashboardProjectApp'));
+
+ var ProjectDetailComponent, scope;
+
+ // Initialize the controller and a mock scope
+ beforeEach(inject(function ($componentController, $rootScope) {
+ scope = $rootScope.$new();
+ ProjectDetailComponent = $componentController('ProjectDetailComponent', {
+ $scope: scope
+ });
+ }));
+
+ it('should ...', function () {
+ expect(1).to.equal(1);
+ });
+});
diff --git a/dashboard-project-app/client/app/projectDetail/views/projectDetail.html b/dashboard-project-app/client/app/projectDetail/views/projectDetail.html
new file mode 100755
index 0000000..ea10dc5
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/views/projectDetail.html
@@ -0,0 +1,12 @@
+
+
+
diff --git a/dashboard-project-app/client/app/projectDetail/views/quickResume.html b/dashboard-project-app/client/app/projectDetail/views/quickResume.html
new file mode 100755
index 0000000..8d288d3
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/views/quickResume.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ {{userStory.title}}
+
+ {{userStory.status | removeDashes | capitalize}}
+
+
+
+
+
+ ID: {{userStory.id}}
+
+
+
+
+
+
+
+
+ Created on {{userStory.createdOn}}
+
+
+
+ Updated on {{userStory.updatedOn}}
+
+
+
+
+
+
+ {{userStory.percentageComplete}}% Complete
+
+
+
+
+
+
+
diff --git a/dashboard-project-app/client/app/projectDetail/views/task.html b/dashboard-project-app/client/app/projectDetail/views/task.html
new file mode 100755
index 0000000..5c349d1
--- /dev/null
+++ b/dashboard-project-app/client/app/projectDetail/views/task.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+ {{key}}
+
+ {{task.xp_status | removeDashes | capitalize}}
+
+
+
+
+
+
+
+
+
+
+
+
+ Projects
+
+
+
+
+
+
+
+
+ Spec
+ Status
+
+
+
+
+
+ {{task.projects_status[actualProject[key]].spec_name | removeDashes | capitalize}}
+
+
+ {{task.projects_status[actualProject[key]].spec_status | removeDashes | capitalize}}
+
+
+
+
+
+ Show details
+
+
+
+
+
+
+
+ Blueprints
+ Status
+ Review Links
+
+
+
+
+
+
+ {{blueprint.name | capitalize | capitalize}}
+
+
+
+
+ {{blueprint.status | removeDashes | capitalize}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard-project-app/client/app/projectList/controllers/projectList.controller.js b/dashboard-project-app/client/app/projectList/controllers/projectList.controller.js
new file mode 100755
index 0000000..514ea3a
--- /dev/null
+++ b/dashboard-project-app/client/app/projectList/controllers/projectList.controller.js
@@ -0,0 +1,42 @@
+'use strict';
+
+angular.module('dashboardProjectApp').controller('projectListCtrl', function(
+ $scope,
+ UserStory
+){
+ var data = [];
+ var lastUpdate;
+ function getFiles() {
+ UserStory.find(
+ function success(userStories, fillTable) {
+ new Promise(function(resolve, reject) {
+ var stringDate, realDate;
+
+ userStories.forEach(function each (story) {
+ if(story.latestUpdate !== undefined) {
+ lastUpdate = moment(story.latestUpdate[0].value).format("MM-DD-YYYY");
+ } else {
+ lastUpdate = moment(story.date, "DD-MM-YYYY").format("MM-DD-YYYY");
+ }
+ data.push(
+ {
+ userStory: story.id+'-'+story.description,
+ dateCreated: moment(story.date, "DD-MM-YYYY").format("MM-DD-YYYY"),
+ lastUpdate: lastUpdate,
+ completed: story.percentage
+ }
+ )
+ resolve(data);
+ });
+ })
+ .then(function(result) {
+ $(function () {
+ $('#table').bootstrapTable({
+ data: data
+ });
+ });
+ });
+ });
+ };
+ getFiles();
+});
diff --git a/dashboard-project-app/client/app/projectList/projectList.js b/dashboard-project-app/client/app/projectList/projectList.js
new file mode 100755
index 0000000..a55d8cc
--- /dev/null
+++ b/dashboard-project-app/client/app/projectList/projectList.js
@@ -0,0 +1,10 @@
+'use strict';
+
+angular.module('dashboardProjectApp').config(function ($stateProvider) {
+ $stateProvider
+ .state('projectList', {
+ url: '/projectList',
+ templateUrl: 'app/projectList/views/projectList.html',
+ controller: 'projectListCtrl'
+ });
+});
diff --git a/dashboard-project-app/client/app/projectList/styles/projectList.css b/dashboard-project-app/client/app/projectList/styles/projectList.css
new file mode 100755
index 0000000..6f8a01c
--- /dev/null
+++ b/dashboard-project-app/client/app/projectList/styles/projectList.css
@@ -0,0 +1,25 @@
+/*
+* Dashboard project classes "dp"
+*/
+.dp-top-alignment {
+ padding-top: 20px;
+}
+.dp-progress {
+ margin-bottom: 0px;
+ background-color: #E6E6E6;
+}
+.pagination-detail {
+ font-size: 12px;
+}
+.search {
+ width: 50%;
+}
+.fixed-table-container tbody td {
+ border-left: none;
+}
+.fixed-table-container thead th{
+ border-left: none;
+}
+.fixed-table-toolbar .bars, .fixed-table-toolbar .search, .fixed-table-toolbar .columns{
+ margin-bottom: 25px;
+}
diff --git a/dashboard-project-app/client/app/projectList/tests/projectList.controller.spec.js b/dashboard-project-app/client/app/projectList/tests/projectList.controller.spec.js
new file mode 100755
index 0000000..b7868bd
--- /dev/null
+++ b/dashboard-project-app/client/app/projectList/tests/projectList.controller.spec.js
@@ -0,0 +1,21 @@
+'use strict';
+
+describe('Component: ProjectListComponent', function () {
+
+ // load the controller's module
+ beforeEach(module('dashboardProjectApp'));
+
+ var ProjectListComponent, scope;
+
+ // Initialize the controller and a mock scope
+ beforeEach(inject(function ($componentController, $rootScope) {
+ scope = $rootScope.$new();
+ ProjectListComponent = $componentController('ProjectListComponent', {
+ $scope: scope
+ });
+ }));
+
+ it('should ...', function () {
+ expect(1).to.equal(1);
+ });
+});
diff --git a/dashboard-project-app/client/app/projectList/views/projectList.html b/dashboard-project-app/client/app/projectList/views/projectList.html
new file mode 100755
index 0000000..814bca9
--- /dev/null
+++ b/dashboard-project-app/client/app/projectList/views/projectList.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ User Story
+ Date Created
+ Last Update
+ % Completed
+
+
+
+
+
+
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/LICENSE.txt b/dashboard-project-app/client/assets/fonts/open-sans/LICENSE.txt
new file mode 100755
index 0000000..d645695
--- /dev/null
+++ b/dashboard-project-app/client/assets/fonts/open-sans/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Bold.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Bold.ttf
new file mode 100755
index 0000000..fd79d43
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Bold.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-BoldItalic.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-BoldItalic.ttf
new file mode 100755
index 0000000..9bc8009
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-BoldItalic.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-ExtraBold.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-ExtraBold.ttf
new file mode 100755
index 0000000..21f6f84
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-ExtraBold.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-ExtraBoldItalic.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-ExtraBoldItalic.ttf
new file mode 100755
index 0000000..31cb688
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-ExtraBoldItalic.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Italic.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Italic.ttf
new file mode 100755
index 0000000..c90da48
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Italic.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Light.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Light.ttf
new file mode 100755
index 0000000..0d38189
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Light.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-LightItalic.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-LightItalic.ttf
new file mode 100755
index 0000000..68299c4
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-LightItalic.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Regular.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Regular.ttf
new file mode 100755
index 0000000..db43334
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Regular.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Semibold.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Semibold.ttf
new file mode 100755
index 0000000..1a7679e
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-Semibold.ttf differ
diff --git a/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-SemiboldItalic.ttf b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-SemiboldItalic.ttf
new file mode 100755
index 0000000..59b6d16
Binary files /dev/null and b/dashboard-project-app/client/assets/fonts/open-sans/OpenSans-SemiboldItalic.ttf differ
diff --git a/dashboard-project-app/client/assets/images/openstack-logo-full.png b/dashboard-project-app/client/assets/images/openstack-logo-full.png
new file mode 100755
index 0000000..e1426e5
Binary files /dev/null and b/dashboard-project-app/client/assets/images/openstack-logo-full.png differ
diff --git a/dashboard-project-app/client/assets/images/yeoman.png b/dashboard-project-app/client/assets/images/yeoman.png
new file mode 100755
index 0000000..7d0a1ac
Binary files /dev/null and b/dashboard-project-app/client/assets/images/yeoman.png differ
diff --git a/dashboard-project-app/client/components/footer/footer.css b/dashboard-project-app/client/components/footer/footer.css
new file mode 100755
index 0000000..cd1753e
--- /dev/null
+++ b/dashboard-project-app/client/components/footer/footer.css
@@ -0,0 +1,6 @@
+footer.footer {
+ text-align: center;
+ padding: 30px 0;
+ margin-top: 70px;
+ border-top: 1px solid #E5E5E5;
+}
diff --git a/dashboard-project-app/client/components/footer/footer.directive.js b/dashboard-project-app/client/components/footer/footer.directive.js
new file mode 100755
index 0000000..fbaa3fe
--- /dev/null
+++ b/dashboard-project-app/client/components/footer/footer.directive.js
@@ -0,0 +1,12 @@
+'use strict';
+
+angular.module('dashboardProjectApp')
+ .directive('footer', function() {
+ return {
+ templateUrl: 'components/footer/footer.html',
+ restrict: 'E',
+ link: function(scope, element) {
+ element.addClass('footer');
+ }
+ };
+ });
diff --git a/dashboard-project-app/client/components/footer/footer.html b/dashboard-project-app/client/components/footer/footer.html
new file mode 100755
index 0000000..fcbe358
--- /dev/null
+++ b/dashboard-project-app/client/components/footer/footer.html
@@ -0,0 +1,6 @@
+
diff --git a/dashboard-project-app/client/components/modal/modal.css b/dashboard-project-app/client/components/modal/modal.css
new file mode 100755
index 0000000..ae04068
--- /dev/null
+++ b/dashboard-project-app/client/components/modal/modal.css
@@ -0,0 +1,23 @@
+.modal-primary .modal-header,
+.modal-info .modal-header,
+.modal-success .modal-header,
+.modal-warning .modal-header,
+.modal-danger .modal-header {
+ color: #fff;
+ border-radius: 5px 5px 0 0;
+}
+.modal-primary .modal-header {
+ background: #428bca;
+}
+.modal-info .modal-header {
+ background: #5bc0de;
+}
+.modal-success .modal-header {
+ background: #5cb85c;
+}
+.modal-warning .modal-header {
+ background: #f0ad4e;
+}
+.modal-danger .modal-header {
+ background: #d9534f;
+}
diff --git a/dashboard-project-app/client/components/modal/modal.html b/dashboard-project-app/client/components/modal/modal.html
new file mode 100755
index 0000000..f04d0db
--- /dev/null
+++ b/dashboard-project-app/client/components/modal/modal.html
@@ -0,0 +1,11 @@
+
+
+
diff --git a/dashboard-project-app/client/components/modal/modal.service.js b/dashboard-project-app/client/components/modal/modal.service.js
new file mode 100755
index 0000000..51eb77b
--- /dev/null
+++ b/dashboard-project-app/client/components/modal/modal.service.js
@@ -0,0 +1,73 @@
+'use strict';
+
+angular.module('dashboardProjectApp')
+ .factory('Modal', function($rootScope, $uibModal) {
+ /**
+ * Opens a modal
+ * @param {Object} scope - an object to be merged with modal's scope
+ * @param {String} modalClass - (optional) class(es) to be applied to the modal
+ * @return {Object} - the instance $uibModal.open() returns
+ */
+ function openModal(scope = {}, modalClass = 'modal-default') {
+ var modalScope = $rootScope.$new();
+
+ angular.extend(modalScope, scope);
+
+ return $uibModal.open({
+ templateUrl: 'components/modal/modal.html',
+ windowClass: modalClass,
+ scope: modalScope
+ });
+ }
+
+ // Public API here
+ return {
+
+ /* Confirmation modals */
+ confirm: {
+
+ /**
+ * Create a function to open a delete confirmation modal (ex. ng-click='myModalFn(name, arg1, arg2...)')
+ * @param {Function} del - callback, ran when delete is confirmed
+ * @return {Function} - the function to open the modal (ex. myModalFn)
+ */
+ delete(del = angular.noop) {
+ /**
+ * Open a delete confirmation modal
+ * @param {String} name - name or info to show on modal
+ * @param {All} - any additional args are passed straight to del callback
+ */
+ return function() {
+ var args = Array.prototype.slice.call(arguments),
+ name = args.shift(),
+ deleteModal;
+
+ deleteModal = openModal({
+ modal: {
+ dismissable: true,
+ title: 'Confirm Delete',
+ html: 'Are you sure you want to delete ' + name + ' ?
',
+ buttons: [{
+ classes: 'btn-danger',
+ text: 'Delete',
+ click: function(e) {
+ deleteModal.close(e);
+ }
+ }, {
+ classes: 'btn-default',
+ text: 'Cancel',
+ click: function(e) {
+ deleteModal.dismiss(e);
+ }
+ }]
+ }
+ }, 'modal-danger');
+
+ deleteModal.result.then(function(event) {
+ del.apply(event, args);
+ });
+ };
+ }
+ }
+ };
+ });
diff --git a/dashboard-project-app/client/components/navbar/navbar.controller.js b/dashboard-project-app/client/components/navbar/navbar.controller.js
new file mode 100755
index 0000000..4c5be84
--- /dev/null
+++ b/dashboard-project-app/client/components/navbar/navbar.controller.js
@@ -0,0 +1,18 @@
+'use strict';
+
+class NavbarController {
+ //start-non-standard
+ menu = [{
+ 'title': 'Home',
+ 'state': 'main'
+ }];
+
+ isCollapsed = true;
+ //end-non-standard
+
+ constructor() {
+ }
+}
+
+angular.module('dashboardProjectApp')
+ .controller('NavbarController', NavbarController);
diff --git a/dashboard-project-app/client/components/navbar/navbar.directive.js b/dashboard-project-app/client/components/navbar/navbar.directive.js
new file mode 100755
index 0000000..a989021
--- /dev/null
+++ b/dashboard-project-app/client/components/navbar/navbar.directive.js
@@ -0,0 +1,9 @@
+'use strict';
+
+angular.module('dashboardProjectApp')
+ .directive('navbar', () => ({
+ templateUrl: 'components/navbar/navbar.html',
+ restrict: 'E',
+ controller: 'NavbarController',
+ controllerAs: 'nav'
+ }));
diff --git a/dashboard-project-app/client/components/navbar/navbar.html b/dashboard-project-app/client/components/navbar/navbar.html
new file mode 100755
index 0000000..e3013db
--- /dev/null
+++ b/dashboard-project-app/client/components/navbar/navbar.html
@@ -0,0 +1,20 @@
+
diff --git a/dashboard-project-app/client/components/ui-router/ui-router.mock.js b/dashboard-project-app/client/components/ui-router/ui-router.mock.js
new file mode 100755
index 0000000..a5a1bf4
--- /dev/null
+++ b/dashboard-project-app/client/components/ui-router/ui-router.mock.js
@@ -0,0 +1,34 @@
+'use strict';
+
+angular.module('stateMock', []);
+angular.module('stateMock').service('$state', function($q) {
+ this.expectedTransitions = [];
+
+ this.transitionTo = function(stateName) {
+ if (this.expectedTransitions.length > 0) {
+ var expectedState = this.expectedTransitions.shift();
+ if (expectedState !== stateName) {
+ throw Error('Expected transition to state: ' + expectedState + ' but transitioned to ' + stateName);
+ }
+ } else {
+ throw Error('No more transitions were expected! Tried to transition to ' + stateName);
+ }
+ console.log('Mock transition to: ' + stateName);
+ var deferred = $q.defer();
+ var promise = deferred.promise;
+ deferred.resolve();
+ return promise;
+ };
+
+ this.go = this.transitionTo;
+
+ this.expectTransitionTo = function(stateName) {
+ this.expectedTransitions.push(stateName);
+ };
+
+ this.ensureAllTransitionsHappened = function() {
+ if (this.expectedTransitions.length > 0) {
+ throw Error('Not all transitions happened!');
+ }
+ };
+});
diff --git a/dashboard-project-app/client/components/util/util.module.js b/dashboard-project-app/client/components/util/util.module.js
new file mode 100755
index 0000000..532c0c4
--- /dev/null
+++ b/dashboard-project-app/client/components/util/util.module.js
@@ -0,0 +1,3 @@
+'use strict';
+
+angular.module('dashboardProjectApp.util', []);
diff --git a/dashboard-project-app/client/components/util/util.service.js b/dashboard-project-app/client/components/util/util.service.js
new file mode 100755
index 0000000..54ea4e5
--- /dev/null
+++ b/dashboard-project-app/client/components/util/util.service.js
@@ -0,0 +1,65 @@
+'use strict';
+
+(function() {
+
+/**
+ * The Util service is for thin, globally reusable, utility functions
+ */
+function UtilService($window) {
+ var Util = {
+ /**
+ * Return a callback or noop function
+ *
+ * @param {Function|*} cb - a 'potential' function
+ * @return {Function}
+ */
+ safeCb(cb) {
+ return (angular.isFunction(cb)) ? cb : angular.noop;
+ },
+
+ /**
+ * Parse a given url with the use of an anchor element
+ *
+ * @param {String} url - the url to parse
+ * @return {Object} - the parsed url, anchor element
+ */
+ urlParse(url) {
+ var a = document.createElement('a');
+ a.href = url;
+
+ // Special treatment for IE, see http://stackoverflow.com/a/13405933 for details
+ if (a.host === '') {
+ a.href = a.href;
+ }
+
+ return a;
+ },
+
+ /**
+ * Test whether or not a given url is same origin
+ *
+ * @param {String} url - url to test
+ * @param {String|String[]} [origins] - additional origins to test against
+ * @return {Boolean} - true if url is same origin
+ */
+ isSameOrigin(url, origins) {
+ url = Util.urlParse(url);
+ origins = (origins && [].concat(origins)) || [];
+ origins = origins.map(Util.urlParse);
+ origins.push($window.location);
+ origins = origins.filter(function(o) {
+ return url.hostname === o.hostname &&
+ url.port === o.port &&
+ url.protocol === o.protocol;
+ });
+ return (origins.length >= 1);
+ }
+ };
+
+ return Util;
+}
+
+angular.module('dashboardProjectApp.util')
+ .factory('Util', UtilService);
+
+})();
diff --git a/dashboard-project-app/client/favicon.ico b/dashboard-project-app/client/favicon.ico
new file mode 100755
index 0000000..8a163fb
Binary files /dev/null and b/dashboard-project-app/client/favicon.ico differ
diff --git a/dashboard-project-app/client/index.html b/dashboard-project-app/client/index.html
new file mode 100755
index 0000000..7183ec9
--- /dev/null
+++ b/dashboard-project-app/client/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+ OpenStack Cross-project Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard-project-app/client/robots.txt b/dashboard-project-app/client/robots.txt
new file mode 100755
index 0000000..9417495
--- /dev/null
+++ b/dashboard-project-app/client/robots.txt
@@ -0,0 +1,3 @@
+# robotstxt.org
+
+User-agent: *
diff --git a/dashboard-project-app/e2e/components/navbar/navbar.po.js b/dashboard-project-app/e2e/components/navbar/navbar.po.js
new file mode 100755
index 0000000..94b8d14
--- /dev/null
+++ b/dashboard-project-app/e2e/components/navbar/navbar.po.js
@@ -0,0 +1,14 @@
+/**
+ * This file uses the Page Object pattern to define the main page for tests
+ * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ
+ */
+
+'use strict';
+
+var NavbarComponent = function() {
+ this.navbar = element(by.css('.navbar'));
+ this.navbarHeader = this.navbar.element(by.css('.navbar-header'));
+ this.navbarNav = this.navbar.element(by.css('#navbar-main .nav.navbar-nav:not(.navbar-right)'));
+};
+
+module.exports = new NavbarComponent();
diff --git a/dashboard-project-app/e2e/main/main.po.js b/dashboard-project-app/e2e/main/main.po.js
new file mode 100755
index 0000000..6718608
--- /dev/null
+++ b/dashboard-project-app/e2e/main/main.po.js
@@ -0,0 +1,15 @@
+/**
+ * This file uses the Page Object pattern to define the main page for tests
+ * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ
+ */
+
+'use strict';
+
+var MainPage = function() {
+ this.heroEl = element(by.css('.hero-unit'));
+ this.h1El = this.heroEl.element(by.css('h1'));
+ this.imgEl = this.heroEl.element(by.css('img'));
+};
+
+module.exports = new MainPage();
+
diff --git a/dashboard-project-app/e2e/main/main.spec.js b/dashboard-project-app/e2e/main/main.spec.js
new file mode 100755
index 0000000..cf6e337
--- /dev/null
+++ b/dashboard-project-app/e2e/main/main.spec.js
@@ -0,0 +1,19 @@
+'use strict';
+
+var config = browser.params;
+
+describe('Main View', function() {
+ var page;
+
+ beforeEach(function() {
+ let promise = browser.get(config.baseUrl + '/');
+ page = require('./main.po');
+ return promise;
+ });
+
+ it('should include jumbotron with correct data', function() {
+ expect(page.h1El.getText()).to.eventually.equal('\'Allo, \'Allo!');
+ expect(page.imgEl.getAttribute('src')).to.eventually.match(/yeoman.png$/);
+ expect(page.imgEl.getAttribute('alt')).to.eventually.equal('I\'m Yeoman');
+ });
+});
diff --git a/dashboard-project-app/install.sh b/dashboard-project-app/install.sh
new file mode 100755
index 0000000..c0a3273
--- /dev/null
+++ b/dashboard-project-app/install.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+cd ../dashboard-project-api;
+npm install;
+cd ../dashboard-project-app
+npm install;
+bower install;
diff --git a/dashboard-project-app/karma.conf.js b/dashboard-project-app/karma.conf.js
new file mode 100755
index 0000000..d8b5b0f
--- /dev/null
+++ b/dashboard-project-app/karma.conf.js
@@ -0,0 +1,94 @@
+// Karma configuration
+// http://karma-runner.github.io/0.10/config/configuration-file.html
+
+module.exports = function(config) {
+ config.set({
+ // base path, that will be used to resolve files and exclude
+ basePath: '',
+
+ // testing framework to use (jasmine/mocha/qunit/...)
+ frameworks: ['mocha', 'chai', 'sinon-chai', 'chai-as-promised', 'chai-things'],
+
+ client: {
+ mocha: {
+ timeout: 5000 // set default mocha spec timeout
+ }
+ },
+
+ // list of files / patterns to load in the browser
+ files: [
+ // bower:js
+ 'client/bower_components/jquery/dist/jquery.js',
+ 'client/bower_components/angular/angular.js',
+ 'client/bower_components/angular-resource/angular-resource.js',
+ 'client/bower_components/angular-cookies/angular-cookies.js',
+ 'client/bower_components/angular-sanitize/angular-sanitize.js',
+ 'client/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
+ 'client/bower_components/lodash/dist/lodash.compat.js',
+ 'client/bower_components/angular-ui-router/release/angular-ui-router.js',
+ 'client/bower_components/angular-mocks/angular-mocks.js',
+ // endbower
+ 'client/app/app.js',
+ 'client/{app,components}/**/*.module.js',
+ 'client/{app,components}/**/*.js',
+ 'client/{app,components}/**/*.html'
+ ],
+
+ preprocessors: {
+ '**/*.html': 'ng-html2js',
+ 'client/{app,components}/**/*.js': 'babel'
+ },
+
+ ngHtml2JsPreprocessor: {
+ stripPrefix: 'client/'
+ },
+
+ babelPreprocessor: {
+ options: {
+ sourceMap: 'inline'
+ },
+ filename: function (file) {
+ return file.originalPath.replace(/\.js$/, '.es5.js');
+ },
+ sourceFileName: function (file) {
+ return file.originalPath;
+ }
+ },
+
+ // list of files / patterns to exclude
+ exclude: [],
+
+ // web server port
+ port: 8080,
+
+ // level of logging
+ // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+ // reporter types:
+ // - dots
+ // - progress (default)
+ // - spec (karma-spec-reporter)
+ // - junit
+ // - growl
+ // - coverage
+ reporters: ['spec'],
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+ // Start these browsers, currently available:
+ // - Chrome
+ // - ChromeCanary
+ // - Firefox
+ // - Opera
+ // - Safari (only Mac)
+ // - PhantomJS
+ // - IE (only Windows)
+ browsers: ['PhantomJS'],
+
+ // Continuous Integration mode
+ // if true, it capture browsers, run tests and exit
+ singleRun: false
+ });
+};
diff --git a/dashboard-project-app/mocha.conf.js b/dashboard-project-app/mocha.conf.js
new file mode 100755
index 0000000..76f5662
--- /dev/null
+++ b/dashboard-project-app/mocha.conf.js
@@ -0,0 +1,19 @@
+'use strict';
+
+// Register the Babel require hook
+require('babel-core/register');
+
+var chai = require('chai');
+
+// Load Chai assertions
+global.expect = chai.expect;
+global.assert = chai.assert;
+chai.should();
+
+// Load Sinon
+global.sinon = require('sinon');
+
+// Initialize Chai plugins
+chai.use(require('sinon-chai'));
+chai.use(require('chai-as-promised'));
+chai.use(require('chai-things'))
diff --git a/dashboard-project-app/package.json b/dashboard-project-app/package.json
new file mode 100644
index 0000000..cfde499
--- /dev/null
+++ b/dashboard-project-app/package.json
@@ -0,0 +1,105 @@
+{
+ "name": "dashboard-project",
+ "version": "0.0.0",
+ "main": "server/app.js",
+ "dependencies": {
+ "babel-polyfill": "^6.7.2",
+ "babel-runtime": "^6.6.1",
+ "bluebird": "^3.3.3",
+ "body-parser": "^1.13.3",
+ "composable-middleware": "^0.3.0",
+ "compression": "^1.5.2",
+ "connect-mongo": "^0.8.1",
+ "cookie-parser": "^1.3.5",
+ "ejs": "^2.3.3",
+ "errorhandler": "^1.4.2",
+ "express": "^4.13.3",
+ "express-session": "^1.11.3",
+ "lodash": "^4.6.1",
+ "lusca": "^1.3.0",
+ "method-override": "^2.3.5",
+ "mongoose": "^4.1.2",
+ "morgan": "~1.7.0",
+ "serve-favicon": "^2.3.0"
+ },
+ "devDependencies": {
+ "autoprefixer": "^6.0.0",
+ "babel-core": "^6.6.5",
+ "babel-register": "^6.6.5",
+ "babel-plugin-transform-class-properties": "^6.6.0",
+ "babel-plugin-transform-runtime": "^6.6.0",
+ "babel-preset-es2015": "^6.6.0",
+ "grunt": "~0.4.5",
+ "grunt-wiredep": "^2.0.0",
+ "grunt-concurrent": "^2.0.1",
+ "grunt-contrib-clean": "^1.0.0",
+ "grunt-contrib-concat": "^1.0.0",
+ "grunt-contrib-copy": "^1.0.0",
+ "grunt-contrib-cssmin": "^1.0.0",
+ "grunt-contrib-imagemin": "^1.0.0",
+ "grunt-contrib-jshint": "^1.0.0",
+ "grunt-contrib-uglify": "^1.0.0",
+ "grunt-contrib-watch": "~0.6.1",
+ "grunt-babel": "~6.0.0",
+ "grunt-google-cdn": "~0.4.0",
+ "grunt-jscs": "^2.1.0",
+ "grunt-newer": "^1.1.1",
+ "grunt-ng-annotate": "^2.0.1",
+ "grunt-ng-constant": "^2.0.1",
+ "grunt-filerev": "^2.3.1",
+ "grunt-usemin": "^3.0.0",
+ "grunt-env": "~0.4.1",
+ "grunt-node-inspector": "^0.4.1",
+ "grunt-nodemon": "^0.4.0",
+ "grunt-angular-templates": "^1.0.3",
+ "grunt-dom-munger": "^3.4.0",
+ "grunt-protractor-runner": "^2.0.0",
+ "grunt-injector": "^0.6.0",
+ "grunt-karma": "~0.12.0",
+ "grunt-build-control": "^0.6.0",
+ "jit-grunt": "~0.10.0",
+ "grunt-express-server": "^0.5.1",
+ "grunt-postcss": "~0.8.0",
+ "grunt-open": "~0.2.3",
+ "time-grunt": "^1.2.1",
+ "grunt-mocha-test": "~0.12.7",
+ "grunt-mocha-istanbul": "^3.0.1",
+ "open": "~0.0.4",
+ "jshint-stylish": "~2.1.0",
+ "connect-livereload": "^0.5.3",
+ "istanbul": "~0.4.1",
+ "chai": "^3.2.0",
+ "sinon": "^1.16.1",
+ "chai-as-promised": "^5.1.0",
+ "chai-things": "^0.2.0",
+ "karma": "~0.13.3",
+ "karma-ng-scenario": "~0.1.0",
+ "karma-firefox-launcher": "~0.1.6",
+ "karma-script-launcher": "~0.1.0",
+ "karma-chrome-launcher": "~0.2.0",
+ "karma-requirejs": "~0.2.2",
+ "karma-jade-preprocessor": "0.0.11",
+ "karma-phantomjs-launcher": "~1.0.0",
+ "karma-ng-html2js-preprocessor": "~0.2.0",
+ "karma-spec-reporter": "~0.0.20",
+ "sinon-chai": "^2.8.0",
+ "mocha": "^2.2.5",
+ "karma-mocha": "^0.2.0",
+ "karma-chai-plugins": "~0.7.0",
+ "karma-babel-preprocessor": "^6.0.1",
+ "requirejs": "~2.1.11",
+ "phantomjs-prebuilt": "^2.1.4",
+ "proxyquire": "^1.0.1",
+ "supertest": "^1.1.0"
+ },
+ "engines": {
+ "node": "^4.4.0",
+ "npm": "^2.14.20"
+ },
+ "scripts": {
+ "start": "node server",
+ "test": "grunt test",
+ "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update"
+ },
+ "private": true
+}
diff --git a/dashboard-project-app/protractor.conf.js b/dashboard-project-app/protractor.conf.js
new file mode 100755
index 0000000..a9669d9
--- /dev/null
+++ b/dashboard-project-app/protractor.conf.js
@@ -0,0 +1,81 @@
+// Protractor configuration
+// https://github.com/angular/protractor/blob/master/referenceConf.js
+
+'use strict';
+
+var config = {
+ // The timeout for each script run on the browser. This should be longer
+ // than the maximum time your application needs to stabilize between tasks.
+ allScriptsTimeout: 110000,
+
+ // A base URL for your application under test. Calls to protractor.get()
+ // with relative paths will be prepended with this.
+ baseUrl: 'http://localhost:' + (process.env.PORT || '9000'),
+
+ // Credientials for Saucelabs
+ sauceUser: process.env.SAUCE_USERNAME,
+
+ sauceKey: process.env.SAUCE_ACCESS_KEY,
+
+ // list of files / patterns to load in the browser
+ specs: [
+ 'e2e/**/*.spec.js'
+ ],
+
+ // Patterns to exclude.
+ exclude: [],
+
+ // ----- Capabilities to be passed to the webdriver instance ----
+ //
+ // For a full list of available capabilities, see
+ // https://code.google.com/p/selenium/wiki/DesiredCapabilities
+ // and
+ // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js
+ capabilities: {
+ 'browserName': 'chrome',
+ 'name': 'Fullstack E2E',
+ 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
+ 'build': process.env.TRAVIS_BUILD_NUMBER
+ },
+
+ // ----- The test framework -----
+ //
+ // Jasmine and Cucumber are fully supported as a test and assertion framework.
+ // Mocha has limited beta support. You will need to include your own
+ // assertion framework if working with mocha.
+ framework: 'mocha',
+
+ // ----- Options to be passed to mocha -----
+ mochaOpts: {
+ reporter: 'spec',
+ timeout: 30000,
+ defaultTimeoutInterval: 30000
+ },
+
+ // Prepare environment for tests
+ params: {
+ serverConfig: require('./server/config/environment')
+ },
+
+ onPrepare: function() {
+ require('babel-register');
+ // Load Mocha and Chai + plugins
+ require('./mocha.conf');
+
+ // Expose should assertions (see https://github.com/angular/protractor/issues/633)
+ Object.defineProperty(
+ protractor.promise.Promise.prototype,
+ 'should',
+ Object.getOwnPropertyDescriptor(Object.prototype, 'should')
+ );
+
+ var serverConfig = config.params.serverConfig;
+
+ // Setup mongo for tests
+ var mongoose = require('mongoose');
+ mongoose.connect(serverConfig.mongo.uri, serverConfig.mongo.options); // Connect to database
+ }
+};
+
+config.params.baseUrl = config.baseUrl;
+exports.config = config;
diff --git a/dashboard-project-app/server/.jshintrc b/dashboard-project-app/server/.jshintrc
new file mode 100755
index 0000000..69f3b00
--- /dev/null
+++ b/dashboard-project-app/server/.jshintrc
@@ -0,0 +1,15 @@
+{
+ "expr": true,
+ "node": true,
+ "esnext": true,
+ "bitwise": true,
+ "eqeqeq": true,
+ "immed": true,
+ "latedef": "nofunc",
+ "newcap": true,
+ "noarg": true,
+ "undef": true,
+ "smarttabs": true,
+ "asi": true,
+ "debug": true
+}
diff --git a/dashboard-project-app/server/.jshintrc-spec b/dashboard-project-app/server/.jshintrc-spec
new file mode 100755
index 0000000..b9390c3
--- /dev/null
+++ b/dashboard-project-app/server/.jshintrc-spec
@@ -0,0 +1,14 @@
+{
+ "extends": ".jshintrc",
+ "globals": {
+ "describe": true,
+ "it": true,
+ "before": true,
+ "beforeEach": true,
+ "after": true,
+ "afterEach": true,
+ "expect": true,
+ "assert": true,
+ "sinon": true
+ }
+}
diff --git a/dashboard-project-app/server/api/thing/index.js b/dashboard-project-app/server/api/thing/index.js
new file mode 100755
index 0000000..f82400a
--- /dev/null
+++ b/dashboard-project-app/server/api/thing/index.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var express = require('express');
+var controller = require('./thing.controller');
+
+var router = express.Router();
+
+router.get('/', controller.index);
+router.get('/:id', controller.show);
+router.post('/', controller.create);
+router.put('/:id', controller.update);
+router.patch('/:id', controller.update);
+router.delete('/:id', controller.destroy);
+
+module.exports = router;
diff --git a/dashboard-project-app/server/api/thing/index.spec.js b/dashboard-project-app/server/api/thing/index.spec.js
new file mode 100755
index 0000000..890f3cf
--- /dev/null
+++ b/dashboard-project-app/server/api/thing/index.spec.js
@@ -0,0 +1,97 @@
+'use strict';
+
+var proxyquire = require('proxyquire').noPreserveCache();
+
+var thingCtrlStub = {
+ index: 'thingCtrl.index',
+ show: 'thingCtrl.show',
+ create: 'thingCtrl.create',
+ update: 'thingCtrl.update',
+ destroy: 'thingCtrl.destroy'
+};
+
+var routerStub = {
+ get: sinon.spy(),
+ put: sinon.spy(),
+ patch: sinon.spy(),
+ post: sinon.spy(),
+ delete: sinon.spy()
+};
+
+// require the index with our stubbed out modules
+var thingIndex = proxyquire('./index.js', {
+ 'express': {
+ Router: function() {
+ return routerStub;
+ }
+ },
+ './thing.controller': thingCtrlStub
+});
+
+describe('Thing API Router:', function() {
+
+ it('should return an express router instance', function() {
+ expect(thingIndex).to.equal(routerStub);
+ });
+
+ describe('GET /api/things', function() {
+
+ it('should route to thing.controller.index', function() {
+ expect(routerStub.get
+ .withArgs('/', 'thingCtrl.index')
+ ).to.have.been.calledOnce;
+ });
+
+ });
+
+ describe('GET /api/things/:id', function() {
+
+ it('should route to thing.controller.show', function() {
+ expect(routerStub.get
+ .withArgs('/:id', 'thingCtrl.show')
+ ).to.have.been.calledOnce;
+ });
+
+ });
+
+ describe('POST /api/things', function() {
+
+ it('should route to thing.controller.create', function() {
+ expect(routerStub.post
+ .withArgs('/', 'thingCtrl.create')
+ ).to.have.been.calledOnce;
+ });
+
+ });
+
+ describe('PUT /api/things/:id', function() {
+
+ it('should route to thing.controller.update', function() {
+ expect(routerStub.put
+ .withArgs('/:id', 'thingCtrl.update')
+ ).to.have.been.calledOnce;
+ });
+
+ });
+
+ describe('PATCH /api/things/:id', function() {
+
+ it('should route to thing.controller.update', function() {
+ expect(routerStub.patch
+ .withArgs('/:id', 'thingCtrl.update')
+ ).to.have.been.calledOnce;
+ });
+
+ });
+
+ describe('DELETE /api/things/:id', function() {
+
+ it('should route to thing.controller.destroy', function() {
+ expect(routerStub.delete
+ .withArgs('/:id', 'thingCtrl.destroy')
+ ).to.have.been.calledOnce;
+ });
+
+ });
+
+});
diff --git a/dashboard-project-app/server/api/thing/thing.controller.js b/dashboard-project-app/server/api/thing/thing.controller.js
new file mode 100755
index 0000000..8f15856
--- /dev/null
+++ b/dashboard-project-app/server/api/thing/thing.controller.js
@@ -0,0 +1,102 @@
+/**
+ * Using Rails-like standard naming convention for endpoints.
+ * GET /api/things -> index
+ * POST /api/things -> create
+ * GET /api/things/:id -> show
+ * PUT /api/things/:id -> update
+ * DELETE /api/things/:id -> destroy
+ */
+
+'use strict';
+
+import _ from 'lodash';
+import Thing from './thing.model';
+
+function respondWithResult(res, statusCode) {
+ statusCode = statusCode || 200;
+ return function(entity) {
+ if (entity) {
+ res.status(statusCode).json(entity);
+ }
+ };
+}
+
+function saveUpdates(updates) {
+ return function(entity) {
+ var updated = _.merge(entity, updates);
+ return updated.save()
+ .then(updated => {
+ return updated;
+ });
+ };
+}
+
+function removeEntity(res) {
+ return function(entity) {
+ if (entity) {
+ return entity.remove()
+ .then(() => {
+ res.status(204).end();
+ });
+ }
+ };
+}
+
+function handleEntityNotFound(res) {
+ return function(entity) {
+ if (!entity) {
+ res.status(404).end();
+ return null;
+ }
+ return entity;
+ };
+}
+
+function handleError(res, statusCode) {
+ statusCode = statusCode || 500;
+ return function(err) {
+ res.status(statusCode).send(err);
+ };
+}
+
+// Gets a list of Things
+export function index(req, res) {
+ return Thing.find().exec()
+ .then(respondWithResult(res))
+ .catch(handleError(res));
+}
+
+// Gets a single Thing from the DB
+export function show(req, res) {
+ return Thing.findById(req.params.id).exec()
+ .then(handleEntityNotFound(res))
+ .then(respondWithResult(res))
+ .catch(handleError(res));
+}
+
+// Creates a new Thing in the DB
+export function create(req, res) {
+ return Thing.create(req.body)
+ .then(respondWithResult(res, 201))
+ .catch(handleError(res));
+}
+
+// Updates an existing Thing in the DB
+export function update(req, res) {
+ if (req.body._id) {
+ delete req.body._id;
+ }
+ return Thing.findById(req.params.id).exec()
+ .then(handleEntityNotFound(res))
+ .then(saveUpdates(req.body))
+ .then(respondWithResult(res))
+ .catch(handleError(res));
+}
+
+// Deletes a Thing from the DB
+export function destroy(req, res) {
+ return Thing.findById(req.params.id).exec()
+ .then(handleEntityNotFound(res))
+ .then(removeEntity(res))
+ .catch(handleError(res));
+}
diff --git a/dashboard-project-app/server/api/thing/thing.events.js b/dashboard-project-app/server/api/thing/thing.events.js
new file mode 100755
index 0000000..4e2e2d6
--- /dev/null
+++ b/dashboard-project-app/server/api/thing/thing.events.js
@@ -0,0 +1,33 @@
+/**
+ * Thing model events
+ */
+
+'use strict';
+
+import {EventEmitter} from 'events';
+import Thing from './thing.model';
+var ThingEvents = new EventEmitter();
+
+// Set max event listeners (0 == unlimited)
+ThingEvents.setMaxListeners(0);
+
+// Model events
+var events = {
+ 'save': 'save',
+ 'remove': 'remove'
+};
+
+// Register the event emitter to the model events
+for (var e in events) {
+ var event = events[e];
+ Thing.schema.post(e, emitEvent(event));
+}
+
+function emitEvent(event) {
+ return function(doc) {
+ ThingEvents.emit(event + ':' + doc._id, doc);
+ ThingEvents.emit(event, doc);
+ }
+}
+
+export default ThingEvents;
diff --git a/dashboard-project-app/server/api/thing/thing.integration.js b/dashboard-project-app/server/api/thing/thing.integration.js
new file mode 100755
index 0000000..9021d07
--- /dev/null
+++ b/dashboard-project-app/server/api/thing/thing.integration.js
@@ -0,0 +1,147 @@
+'use strict';
+
+var app = require('../..');
+import request from 'supertest';
+
+var newThing;
+
+describe('Thing API:', function() {
+
+ describe('GET /api/things', function() {
+ var things;
+
+ beforeEach(function(done) {
+ request(app)
+ .get('/api/things')
+ .expect(200)
+ .expect('Content-Type', /json/)
+ .end((err, res) => {
+ if (err) {
+ return done(err);
+ }
+ things = res.body;
+ done();
+ });
+ });
+
+ it('should respond with JSON array', function() {
+ expect(things).to.be.instanceOf(Array);
+ });
+
+ });
+
+ describe('POST /api/things', function() {
+ beforeEach(function(done) {
+ request(app)
+ .post('/api/things')
+ .send({
+ name: 'New Thing',
+ info: 'This is the brand new thing!!!'
+ })
+ .expect(201)
+ .expect('Content-Type', /json/)
+ .end((err, res) => {
+ if (err) {
+ return done(err);
+ }
+ newThing = res.body;
+ done();
+ });
+ });
+
+ it('should respond with the newly created thing', function() {
+ expect(newThing.name).to.equal('New Thing');
+ expect(newThing.info).to.equal('This is the brand new thing!!!');
+ });
+
+ });
+
+ describe('GET /api/things/:id', function() {
+ var thing;
+
+ beforeEach(function(done) {
+ request(app)
+ .get('/api/things/' + newThing._id)
+ .expect(200)
+ .expect('Content-Type', /json/)
+ .end((err, res) => {
+ if (err) {
+ return done(err);
+ }
+ thing = res.body;
+ done();
+ });
+ });
+
+ afterEach(function() {
+ thing = {};
+ });
+
+ it('should respond with the requested thing', function() {
+ expect(thing.name).to.equal('New Thing');
+ expect(thing.info).to.equal('This is the brand new thing!!!');
+ });
+
+ });
+
+ describe('PUT /api/things/:id', function() {
+ var updatedThing;
+
+ beforeEach(function(done) {
+ request(app)
+ .put('/api/things/' + newThing._id)
+ .send({
+ name: 'Updated Thing',
+ info: 'This is the updated thing!!!'
+ })
+ .expect(200)
+ .expect('Content-Type', /json/)
+ .end(function(err, res) {
+ if (err) {
+ return done(err);
+ }
+ updatedThing = res.body;
+ done();
+ });
+ });
+
+ afterEach(function() {
+ updatedThing = {};
+ });
+
+ it('should respond with the updated thing', function() {
+ expect(updatedThing.name).to.equal('Updated Thing');
+ expect(updatedThing.info).to.equal('This is the updated thing!!!');
+ });
+
+ });
+
+ describe('DELETE /api/things/:id', function() {
+
+ it('should respond with 204 on successful removal', function(done) {
+ request(app)
+ .delete('/api/things/' + newThing._id)
+ .expect(204)
+ .end((err, res) => {
+ if (err) {
+ return done(err);
+ }
+ done();
+ });
+ });
+
+ it('should respond with 404 when thing does not exist', function(done) {
+ request(app)
+ .delete('/api/things/' + newThing._id)
+ .expect(404)
+ .end((err, res) => {
+ if (err) {
+ return done(err);
+ }
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/dashboard-project-app/server/api/thing/thing.model.js b/dashboard-project-app/server/api/thing/thing.model.js
new file mode 100755
index 0000000..9514577
--- /dev/null
+++ b/dashboard-project-app/server/api/thing/thing.model.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import mongoose from 'mongoose';
+
+var ThingSchema = new mongoose.Schema({
+ name: String,
+ info: String,
+ active: Boolean
+});
+
+export default mongoose.model('Thing', ThingSchema);
diff --git a/dashboard-project-app/server/app.js b/dashboard-project-app/server/app.js
new file mode 100755
index 0000000..693a730
--- /dev/null
+++ b/dashboard-project-app/server/app.js
@@ -0,0 +1,39 @@
+/**
+ * Main application file
+ */
+
+'use strict';
+
+import express from 'express';
+import mongoose from 'mongoose';
+mongoose.Promise = require('bluebird');
+import config from './config/environment';
+import http from 'http';
+
+// Connect to MongoDB
+mongoose.connect(config.mongo.uri, config.mongo.options);
+mongoose.connection.on('error', function(err) {
+ console.error('MongoDB connection error: ' + err);
+ process.exit(-1);
+});
+
+// Populate databases with sample data
+if (config.seedDB) { require('./config/seed'); }
+
+// Setup server
+var app = express();
+var server = http.createServer(app);
+require('./config/express').default(app);
+require('./routes').default(app);
+
+// Start server
+function startServer() {
+ app.angularFullstack = server.listen(config.port, config.ip, function() {
+ console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
+ });
+}
+
+setImmediate(startServer);
+
+// Expose app
+exports = module.exports = app;
diff --git a/dashboard-project-app/server/components/errors/index.js b/dashboard-project-app/server/components/errors/index.js
new file mode 100755
index 0000000..5986981
--- /dev/null
+++ b/dashboard-project-app/server/components/errors/index.js
@@ -0,0 +1,22 @@
+/**
+ * Error responses
+ */
+
+'use strict';
+
+module.exports[404] = function pageNotFound(req, res) {
+ var viewFilePath = '404';
+ var statusCode = 404;
+ var result = {
+ status: statusCode
+ };
+
+ res.status(result.status);
+ res.render(viewFilePath, {}, function(err, html) {
+ if (err) {
+ return res.status(result.status).json(result);
+ }
+
+ res.send(html);
+ });
+};
diff --git a/dashboard-project-app/server/config/environment/development.js b/dashboard-project-app/server/config/environment/development.js
new file mode 100755
index 0000000..f6ee702
--- /dev/null
+++ b/dashboard-project-app/server/config/environment/development.js
@@ -0,0 +1,15 @@
+'use strict';
+
+// Development specific configuration
+// ==================================
+module.exports = {
+
+ // MongoDB connection options
+ mongo: {
+ uri: 'mongodb://localhost/dashboardproject-dev'
+ },
+
+ // Seed database on startup
+ seedDB: true
+
+};
diff --git a/dashboard-project-app/server/config/environment/index.js b/dashboard-project-app/server/config/environment/index.js
new file mode 100755
index 0000000..f17e8ee
--- /dev/null
+++ b/dashboard-project-app/server/config/environment/index.js
@@ -0,0 +1,50 @@
+'use strict';
+
+var path = require('path');
+var _ = require('lodash');
+
+function requiredProcessEnv(name) {
+ if (!process.env[name]) {
+ throw new Error('You must set the ' + name + ' environment variable');
+ }
+ return process.env[name];
+}
+
+// All configurations will extend these options
+// ============================================
+var all = {
+ env: process.env.NODE_ENV,
+
+ // Root path of server
+ root: path.normalize(__dirname + '/../../..'),
+
+ // Server port
+ port: process.env.PORT || 9000,
+
+ // Server IP
+ ip: process.env.IP || '0.0.0.0',
+
+ // Should we populate the DB with sample data?
+ seedDB: false,
+
+ // Secret for session, you will want to change this and make it an environment variable
+ secrets: {
+ session: 'dashboard-project-secret'
+ },
+
+ // MongoDB connection options
+ mongo: {
+ options: {
+ db: {
+ safe: true
+ }
+ }
+ }
+};
+
+// Export the config object based on the NODE_ENV
+// ==============================================
+module.exports = _.merge(
+ all,
+ require('./shared'),
+ require('./' + process.env.NODE_ENV + '.js') || {});
diff --git a/dashboard-project-app/server/config/environment/production.js b/dashboard-project-app/server/config/environment/production.js
new file mode 100755
index 0000000..a33eb80
--- /dev/null
+++ b/dashboard-project-app/server/config/environment/production.js
@@ -0,0 +1,24 @@
+'use strict';
+
+// Production specific configuration
+// =================================
+module.exports = {
+ // Server IP
+ ip: process.env.OPENSHIFT_NODEJS_IP ||
+ process.env.IP ||
+ undefined,
+
+ // Server port
+ port: process.env.OPENSHIFT_NODEJS_PORT ||
+ process.env.PORT ||
+ 8080,
+
+ // MongoDB connection options
+ mongo: {
+ uri: process.env.MONGOLAB_URI ||
+ process.env.MONGOHQ_URL ||
+ process.env.OPENSHIFT_MONGODB_DB_URL +
+ process.env.OPENSHIFT_APP_NAME ||
+ 'mongodb://localhost/dashboardproject'
+ }
+};
diff --git a/dashboard-project-app/server/config/environment/shared.js b/dashboard-project-app/server/config/environment/shared.js
new file mode 100755
index 0000000..64a5eab
--- /dev/null
+++ b/dashboard-project-app/server/config/environment/shared.js
@@ -0,0 +1,6 @@
+'use strict';
+
+exports = module.exports = {
+ // List of user roles
+ userRoles: ['guest', 'user', 'admin']
+};
diff --git a/dashboard-project-app/server/config/environment/test.js b/dashboard-project-app/server/config/environment/test.js
new file mode 100755
index 0000000..6e4ca7a
--- /dev/null
+++ b/dashboard-project-app/server/config/environment/test.js
@@ -0,0 +1,20 @@
+'use strict';
+
+// Test specific configuration
+// ===========================
+module.exports = {
+ // MongoDB connection options
+ mongo: {
+ uri: 'mongodb://localhost/dashboardproject-test'
+ },
+ sequelize: {
+ uri: 'sqlite://',
+ options: {
+ logging: false,
+ storage: 'test.sqlite',
+ define: {
+ timestamps: false
+ }
+ }
+ }
+};
diff --git a/dashboard-project-app/server/config/express.js b/dashboard-project-app/server/config/express.js
new file mode 100755
index 0000000..836d5d0
--- /dev/null
+++ b/dashboard-project-app/server/config/express.js
@@ -0,0 +1,85 @@
+/**
+ * Express configuration
+ */
+
+'use strict';
+
+import express from 'express';
+import favicon from 'serve-favicon';
+import morgan from 'morgan';
+import compression from 'compression';
+import bodyParser from 'body-parser';
+import methodOverride from 'method-override';
+import cookieParser from 'cookie-parser';
+import errorHandler from 'errorhandler';
+import path from 'path';
+import lusca from 'lusca';
+import config from './environment';
+import session from 'express-session';
+import connectMongo from 'connect-mongo';
+import mongoose from 'mongoose';
+var MongoStore = connectMongo(session);
+
+export default function(app) {
+ var env = app.get('env');
+
+ app.set('views', config.root + '/server/views');
+ app.engine('html', require('ejs').renderFile);
+ app.set('view engine', 'html');
+ app.use(compression());
+ app.use(bodyParser.urlencoded({ extended: false }));
+ app.use(bodyParser.json());
+ app.use(methodOverride());
+ app.use(cookieParser());
+
+ // Persist sessions with MongoStore / sequelizeStore
+ // We need to enable sessions for passport-twitter because it's an
+ // oauth 1.0 strategy, and Lusca depends on sessions
+ app.use(session({
+ secret: config.secrets.session,
+ saveUninitialized: true,
+ resave: false,
+ store: new MongoStore({
+ mongooseConnection: mongoose.connection,
+ db: 'dashboard-project'
+ })
+ }));
+
+ /**
+ * Lusca - express server security
+ * https://github.com/krakenjs/lusca
+ */
+ if ('test' !== env) {
+ app.use(lusca({
+ csrf: {
+ angular: true
+ },
+ xframe: 'SAMEORIGIN',
+ hsts: {
+ maxAge: 31536000, //1 year, in seconds
+ includeSubDomains: true,
+ preload: true
+ },
+ xssProtection: true
+ }));
+ }
+
+ app.set('appPath', path.join(config.root, 'client'));
+
+ if ('production' === env) {
+ app.use(favicon(path.join(config.root, 'client', 'favicon.ico')));
+ app.use(express.static(app.get('appPath')));
+ app.use(morgan('dev'));
+ }
+
+ if ('development' === env) {
+ app.use(require('connect-livereload')());
+ }
+
+ if ('development' === env || 'test' === env) {
+ app.use(express.static(path.join(config.root, '.tmp')));
+ app.use(express.static(app.get('appPath')));
+ app.use(morgan('dev'));
+ app.use(errorHandler()); // Error handler - has to be last
+ }
+}
diff --git a/dashboard-project-app/server/config/local.env.sample.js b/dashboard-project-app/server/config/local.env.sample.js
new file mode 100755
index 0000000..33c3b7c
--- /dev/null
+++ b/dashboard-project-app/server/config/local.env.sample.js
@@ -0,0 +1,14 @@
+'use strict';
+
+// Use local.env.js for environment variables that grunt will set when the server starts locally.
+// Use for your api keys, secrets, etc. This file should not be tracked by git.
+//
+// You will need to set these on the server you deploy to.
+
+module.exports = {
+ DOMAIN: 'http://localhost:9000',
+ SESSION_SECRET: 'dashboardproject-secret',
+
+ // Control debug level for modules using visionmedia/debug
+ DEBUG: ''
+};
diff --git a/dashboard-project-app/server/config/seed.js b/dashboard-project-app/server/config/seed.js
new file mode 100755
index 0000000..0a0567d
--- /dev/null
+++ b/dashboard-project-app/server/config/seed.js
@@ -0,0 +1,40 @@
+/**
+ * Populate DB with sample data on server start
+ * to disable, edit config/environment/index.js, and set `seedDB: false`
+ */
+
+'use strict';
+import Thing from '../api/thing/thing.model';
+
+Thing.find({}).remove()
+ .then(() => {
+ Thing.create({
+ name: 'Development Tools',
+ info: 'Integration with popular tools such as Bower, Grunt, Babel, Karma, ' +
+ 'Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, ' +
+ 'Stylus, Sass, and Less.'
+ }, {
+ name: 'Server and Client integration',
+ info: 'Built with a powerful and fun stack: MongoDB, Express, ' +
+ 'AngularJS, and Node.'
+ }, {
+ name: 'Smart Build System',
+ info: 'Build system ignores `spec` files, allowing you to keep ' +
+ 'tests alongside code. Automatic injection of scripts and ' +
+ 'styles into your index.html'
+ }, {
+ name: 'Modular Structure',
+ info: 'Best practice client and server structures allow for more ' +
+ 'code reusability and maximum scalability'
+ }, {
+ name: 'Optimized Build',
+ info: 'Build process packs up your templates as a single JavaScript ' +
+ 'payload, minifies your scripts/css/images, and rewrites asset ' +
+ 'names for caching.'
+ }, {
+ name: 'Deployment Ready',
+ info: 'Easily deploy your app to Heroku or Openshift with the heroku ' +
+ 'and openshift subgenerators'
+ });
+ });
+
diff --git a/dashboard-project-app/server/index.js b/dashboard-project-app/server/index.js
new file mode 100755
index 0000000..c3ee524
--- /dev/null
+++ b/dashboard-project-app/server/index.js
@@ -0,0 +1,12 @@
+'use strict';
+
+// Set default node environment to development
+var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development';
+
+if (env === 'development' || env === 'test') {
+ // Register the Babel require hook
+ require('babel-register');
+}
+
+// Export the application
+exports = module.exports = require('./app');
diff --git a/dashboard-project-app/server/routes.js b/dashboard-project-app/server/routes.js
new file mode 100755
index 0000000..f67f143
--- /dev/null
+++ b/dashboard-project-app/server/routes.js
@@ -0,0 +1,22 @@
+/**
+ * Main application routes
+ */
+
+'use strict';
+
+import errors from './components/errors';
+import path from 'path';
+
+export default function(app) {
+ // Insert routes below
+ app.use('/api/things', require('./api/thing'));
+ // All undefined asset or api routes should return a 404
+ app.route('/:url(api|auth|components|app|bower_components|assets)/*')
+ .get(errors[404]);
+
+ // All other routes should redirect to the index.html
+ app.route('/*')
+ .get((req, res) => {
+ res.sendFile(path.resolve(app.get('appPath') + '/index.html'));
+ });
+}
diff --git a/dashboard-project-app/server/views/404.html b/dashboard-project-app/server/views/404.html
new file mode 100755
index 0000000..ec98e3c
--- /dev/null
+++ b/dashboard-project-app/server/views/404.html
@@ -0,0 +1,157 @@
+
+
+
+
+ Page Not Found :(
+
+
+
+
+
Not found :(
+
Sorry, but the page you were trying to view does not exist.
+
It looks like this was the result of either:
+
+ a mistyped address
+ an out-of-date link
+
+
+
+
+
+
diff --git a/tracker/capacity-management-tracker.json b/tracker/capacity-management-tracker.json
new file mode 100644
index 0000000..3908fd1
--- /dev/null
+++ b/tracker/capacity-management-tracker.json
@@ -0,0 +1,33 @@
+{
+ "id": "0002",
+ "date": "05-04-2016",
+ "submitted_by": {
+ "name": "Shamail Tahir",
+ "email": "itzshamail@gmail.com"
+ },
+ "description": "Capacity Management",
+ "source": "capacity_management",
+ "status": "in-progress",
+ "tasks": [
+ "Service Catalog Standardization"
+ ],
+ "tasks_status": {
+ "Service Catalog Standardization": {
+ "cross-project spec": "service-catalog",
+ "gerrit-topic": "",
+ "xp_status": "in-progress",
+ "projects": [
+ "keystone"
+ ],
+ "projects_status": {
+ "keystone": {
+ "blueprints": {
+ "service-catalog-standards": "in-progress"
+ },
+ "spec": "http://specs.openstack.org/openstack/openstack-specs/specs/service-catalog.html",
+ "spec_status": "completed"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tracker/ha-vm-tracker.json b/tracker/ha-vm-tracker.json
new file mode 100644
index 0000000..87bc7ce
--- /dev/null
+++ b/tracker/ha-vm-tracker.json
@@ -0,0 +1,34 @@
+{
+ "id": "0003",
+ "date": "08-04-2016",
+ "submitted_by": {
+ "name": "Yih Leong Sun",
+ "email": "yihleong@gmail.com>"
+ },
+ "description": "High Availability for Virtual Machines",
+ "source": "ha_vm",
+ "status": "in-progress",
+ "tasks": [
+ "clouds.yaml support in clients"
+ ],
+ "tasks_status": {
+ "clouds.yaml support in clients": {
+ "cross-project spec": "clouds-yaml-support",
+ "gerrit-topic": "",
+ "xp_status": "in-progress",
+ "projects": [
+ "nova"
+ ],
+ "projects_status": {
+ "nova": {
+ "blueprints": {
+ "bp/os-vif-library": "completed",
+ "bp/privsep": "completed"
+ },
+ "spec": "http://specs.openstack.org/openstack/openstack-user-stories/user-stories/proposed/capacity_management.html",
+ "spec_status": "completed"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tracker/rollingupgrades-tracker.json b/tracker/rollingupgrades-tracker.json
new file mode 100644
index 0000000..9bb8cd8
--- /dev/null
+++ b/tracker/rollingupgrades-tracker.json
@@ -0,0 +1,75 @@
+{
+ "id": "0001",
+ "date": "23-03-2016",
+ "submitted_by": {
+ "name": "Kenny Johnston",
+ "email": "kencjohnston@gmail.com"
+ },
+ "description": "Rolling Upgrades",
+ "source": "rolling-upgrades",
+ "status": "in-progress",
+ "tasks": [
+ "Versioned Objects",
+ "Online Schema Change"
+ ],
+ "tasks_status": {
+ "Versioned Objects": {
+ "cross-project spec": "chronicles-of-a-dlm",
+ "gerrit-topic": "tooz",
+ "xp_status": "in-progress",
+ "projects": [
+ "nova",
+ "cinder"
+ ],
+ "projects_status": {
+ "nova": {
+ "blueprints": {
+ "bp/service-version-number": "completed"
+ },
+ "spec": "http://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/service-version-number.html",
+ "spec_status": "completed"
+ },
+ "cinder": {
+ "blueprints": {
+ "bp/cinder-object-fields": "completed"
+ },
+ "spec": "https://github.com/openstack/cinder-specs/blob/master/specs/kilo/cinder-objects.rst",
+ "spec_status": "completed"
+ }
+ }
+ },
+ "Online Schema Change": {
+ "cross-project spec": "no-downward-sql-migration",
+ "gerrit-topic": "online-schema-changes",
+ "xp_status": "in-progress",
+ "projects": [
+ "nova",
+ "cinder",
+ "keystone"
+ ],
+ "projects_status": {
+ "nova": {
+ "blueprints": {
+ "online-schema-changes": "completed"
+ },
+ "spec": "http://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/online-schema-changes.html",
+ "spec_status": "completed"
+ },
+ "cinder": {
+ "blueprints": {
+ "bp/online-schema-upgrades": "completed"
+ },
+ "spec": "https://github.com/openstack/cinder-specs/blob/master/specs/kilo/cinder-objects.rst",
+ "spec_status": "completed"
+ },
+ "keystone": {
+ "blueprints": {
+ "bp/online-schema-migration": "pending"
+ },
+ "spec": "http://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/online-schema-changes.html",
+ "spec_status": "pending"
+ }
+ }
+ }
+ }
+}