You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@usergrid.apache.org by sn...@apache.org on 2014/02/05 18:21:33 UTC

[41/83] [abbrv] [partial] updated to latest Angular-based admin portal

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/charts/sparklines.js
----------------------------------------------------------------------
diff --git a/portal/js/charts/sparklines.js b/portal/js/charts/sparklines.js
new file mode 100644
index 0000000..29551c5
--- /dev/null
+++ b/portal/js/charts/sparklines.js
@@ -0,0 +1,2 @@
+$('.sessions-bar').sparkline([ 3, 5, 6, 3, 4, 5, 6, 7, 8, 4, 3, 5, 6, 3, 4, 5, 6, 7, 8, 4, 3, 5, 6, 3, 4, 5, 6, 7, 8, 4, 3, 5, 6, 3, 4, 5, 6, 7, 8, 4, 3, 5, 6, 3, 4, 5, 6, 7, 8, 4, 3, 5, 6, 3, 4, 5, 6, 7, 8, 1 ], {type: 'bar', barColor: '#c5c5c5', width: '800px', height: 100, barWidth: 12, barSpacing: '1px'});
+// $('.sessions-bar canvas').width('100%')
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/data-controller.js
----------------------------------------------------------------------
diff --git a/portal/js/data/data-controller.js b/portal/js/data/data-controller.js
new file mode 100644
index 0000000..6644a4e
--- /dev/null
+++ b/portal/js/data/data-controller.js
@@ -0,0 +1,304 @@
+'use strict'
+
+AppServices.Controllers.controller('DataCtrl', ['ug', '$scope', '$rootScope', '$location',
+  function (ug, $scope, $rootScope, $location) {
+
+    var init = function () {
+      $scope.verb = 'GET';
+      $scope.display = '';
+
+      $scope.queryBodyDetail = {};
+
+      $scope.queryBodyDisplay = 'none';
+      $scope.queryLimitDisplay = 'block';
+      $scope.queryStringDisplay = 'block';
+      $scope.entitySelected = {};
+      $scope.newCollection = {};
+      $rootScope.queryCollection = {};
+      $scope.data = {};
+      $scope.data.queryPath = '';
+      $scope.data.queryBody = '{ "name":"value" }';
+      $scope.data.searchString = '';
+      $scope.data.queryLimit = '';
+    };
+    var runQuery = function(verb){
+      $scope.loading = true;
+      //get the parameters
+      var queryPath = $scope.removeFirstSlash($scope.data.queryPath || '');
+      var searchString = $scope.data.searchString || '';
+      var queryLimit = $scope.data.queryLimit || '';
+      var body = JSON.parse($scope.data.queryBody || '{}');
+
+      if (verb == 'POST' && $scope.validateJson(true)) {
+        ug.runDataPOSTQuery(queryPath, body);
+      } else if (verb == 'PUT' && $scope.validateJson(true)) {
+        ug.runDataPutQuery(queryPath, searchString, queryLimit, body);
+      } else if (verb == 'DELETE') {
+        ug.runDataDeleteQuery(queryPath, searchString, queryLimit);
+      } else {
+        //GET
+        ug.runDataQuery(queryPath, searchString, queryLimit);
+      }
+    };
+
+    $scope.$on('top-collections-received', function(event, collectionList) {
+      $scope.loading = false;
+      var ignoredCollections = ['events'];
+      //remove some elements from the collection
+      ignoredCollections.forEach(function(ignoredCollection){
+         collectionList.hasOwnProperty(ignoredCollection)  //important check that this is objects own property, not from prototype prop inherited
+          && delete collectionList[ignoredCollection]
+         ;
+      });
+      $scope.collectionList = collectionList;
+      $scope.queryBoxesSelected = false;
+      if(!$scope.queryPath){
+        $scope.loadCollection('/'+collectionList[Object.keys(collectionList).sort()[0]].name);
+      }
+      $scope.applyScope();
+    });
+    $scope.$on('error-running-query', function(event) {
+      $scope.loading = false;
+      runQuery('GET');
+      $scope.applyScope();
+    });
+
+
+    $scope.$on('entity-deleted', function(event) {
+      $scope.deleteLoading = false;
+      $rootScope.$broadcast('alert','success', 'Entities deleted sucessfully');
+      $scope.queryBoxesSelected = false;
+      $scope.checkNextPrev();
+      $scope.applyScope();
+
+    });
+    $scope.$on('entity-deleted-error', function(event) {
+      $scope.deleteLoading = false;
+      runQuery('GET');
+      $scope.applyScope();
+    });
+
+    $scope.$on('collection-created',function(){
+      $scope.newCollection.name = '';
+    });
+
+    $scope.$on('query-received', function(event, collection) {
+      $scope.loading = false;
+      $rootScope.queryCollection = collection;
+      ug.getIndexes($scope.data.queryPath);
+      //$rootScope.$broadcast('alert','success', 'API call completed sucessfully');
+
+      $scope.setDisplayType();
+
+      $scope.checkNextPrev();
+      $scope.applyScope();
+      $scope.queryBoxesSelected = false;
+
+    });
+
+    $scope.$on('indexes-received', function(event, indexes) {
+      //todo - do something with the indexes
+      var fred = indexes;
+    });
+
+    $scope.$on('app-changed',function(){
+      init();
+    });
+
+    $scope.setDisplayType = function() {
+      $scope.display = 'generic';
+    }
+
+    $scope.deleteEntitiesDialog = function(modalId){
+      $scope.deleteLoading = false;
+      $scope.deleteEntities($rootScope.queryCollection, 'entity-deleted', 'error deleting entity');
+
+      $scope.hideModal(modalId)
+    };
+
+    $scope.newCollectionDialog = function(modalId){
+      if ($scope.newCollection.name) {
+        ug.createCollection($scope.newCollection.name);
+        ug.getTopCollections();
+        $rootScope.$broadcast('alert', 'success', 'Collection created successfully.');
+        $scope.hideModal(modalId)
+      } else {
+        $rootScope.$broadcast('alert', 'error', 'You must specify a collection name.');
+      }
+    };
+
+    $scope.addToPath = function(uuid){
+      $scope.data.queryPath = '/' + $rootScope.queryCollection._type + '/' + uuid;
+    }
+
+    $scope.isDeep = function(item){
+      //can be better, just see if object for now
+      return (Object.prototype.toString.call(item) === "[object Object]");
+    };
+
+    //get collection list
+    $scope.loadCollection = function(type) {
+      $scope.data.queryPath = '/'+type.substring(1,type.length);
+      $scope.data.searchString = '';
+      $scope.data.queryLimit = '';
+      $scope.data.body = '{ "name":"value" }';
+      $scope.selectGET();
+      $scope.applyScope();
+      $scope.run();
+    }
+    $scope.selectGET = function() {
+      $scope.queryBodyDisplay = 'none';
+      $scope.queryLimitDisplay = 'block';
+      $scope.queryStringDisplay = 'block';
+      $scope.verb = 'GET';
+    }
+    $scope.selectPOST = function() {
+      $scope.queryBodyDisplay = 'block';
+      $scope.queryLimitDisplay = 'none';
+      $scope.queryStringDisplay = 'none';
+      $scope.verb = 'POST';
+    }
+    $scope.selectPUT = function() {
+      $scope.queryBodyDisplay = 'block';
+      $scope.queryLimitDisplay = 'block';
+      $scope.queryStringDisplay = 'block';
+      $scope.verb = 'PUT';
+    }
+    $scope.selectDELETE = function() {
+      $scope.queryBodyDisplay = 'none';
+      $scope.queryLimitDisplay = 'block';
+      $scope.queryStringDisplay = 'block';
+      $scope.verb = 'DELETE';
+    }
+
+    $scope.validateJson = function(skipMessage) {
+      var queryBody = $scope.data.queryBody;
+
+      try {
+        queryBody = JSON.parse(queryBody);
+      } catch (e) {
+        $rootScope.$broadcast('alert', 'error', 'JSON is not valid');
+        return false;
+      }
+
+      queryBody = JSON.stringify(queryBody,null,2);
+
+      !skipMessage && $rootScope.$broadcast('alert','success', 'JSON is valid');
+
+      $scope.data.queryBody = queryBody;
+      return true;
+    }
+
+
+    $scope.saveEntity = function(entity){
+      if (!$scope.validateJson()) {
+        return false;
+      }
+      var queryBody = entity._json;
+      queryBody = JSON.parse(queryBody);
+      $rootScope.selectedEntity.set(); //clears out all entities
+      $rootScope.selectedEntity.set(queryBody);
+      $rootScope.selectedEntity.set('type', entity._data.type);
+      $rootScope.selectedEntity.set('uuid', entity._data.uuid);
+      $rootScope.selectedEntity.save(function(err, data){
+        if (err) {
+          $rootScope.$broadcast('alert', 'error', 'error: ' + data.error_description);
+        } else {
+          $rootScope.$broadcast('alert', 'success', 'entity saved');
+        }
+
+      });
+    }
+
+    $scope.run = function() {
+      //clear the current data
+      $rootScope.queryCollection = '';
+
+      var verb = $scope.verb;
+      runQuery(verb);
+    }
+
+    $scope.hasProperty = function(prop){
+      //checks each object in array for a given property - used for hiding/showing UI data in table
+      var retval = false;
+      if(typeof $rootScope.queryCollection._list !== 'undefined'){
+        angular.forEach($rootScope.queryCollection._list, function(value, key){
+          //no way to break angular loop :(
+          if(!retval){
+            if(value._data[prop]){
+              retval = true;
+            }
+          }
+        });
+      }
+      return retval;
+    }
+
+    $scope.resetNextPrev = function() {
+      $scope.previous_display = 'none';
+      $scope.next_display = 'none';
+    }
+
+    $scope.checkNextPrev = function() {
+      $scope.resetNextPrev();
+      if ($rootScope.queryCollection.hasPreviousPage()) {
+        $scope.previous_display = 'default';
+      }
+      if($rootScope.queryCollection.hasNextPage()) {
+        $scope.next_display = 'default';
+      }
+    }
+
+    $scope.selectEntity = function(uuid){
+      $rootScope.selectedEntity = $rootScope.queryCollection.getEntityByUUID(uuid);
+      $scope.addToPath(uuid);
+    }
+
+    $scope.getJSONView = function(entity){
+      var tempjson  = entity.get();
+
+      //rip out the system elements.  Stringify first because we don't want to rip them out of the actual object
+      var queryBody = JSON.stringify(tempjson, null, 2);
+      queryBody = JSON.parse(queryBody);
+      delete queryBody.metadata;
+      delete queryBody.uuid;
+      delete queryBody.created;
+      delete queryBody.modified;
+      delete queryBody.type;
+
+      $scope.queryBody = JSON.stringify(queryBody, null, 2);
+    }
+
+    $scope.getPrevious = function () {
+      $rootScope.queryCollection.getPreviousPage(function(err) {
+        if (err) {
+          $rootScope.$broadcast('alert', 'error', 'error getting previous page of data');
+        }
+        $scope.checkNextPrev();
+        $scope.applyScope();
+      });
+    };
+
+    $scope.getNext = function () {
+      $rootScope.queryCollection.getNextPage(function(err) {
+        if (err) {
+          $rootScope.$broadcast('alert', 'error', 'error getting next page of data');
+        }
+        $scope.checkNextPrev();
+        $scope.applyScope();
+      });
+    };
+
+    init();
+    //persistent vars (stay around when you drill down to an entity detail view)
+    $rootScope.queryCollection = $rootScope.queryCollection  || {};
+    $rootScope.selectedEntity = {};
+    if ($rootScope.queryCollection && $rootScope.queryCollection._type) {
+      $scope.loadCollection($rootScope.queryCollection._type);
+      $scope.setDisplayType();
+    }
+    ug.getTopCollections();
+    $scope.resetNextPrev();
+
+
+  }]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/data.html
----------------------------------------------------------------------
diff --git a/portal/js/data/data.html b/portal/js/data/data.html
new file mode 100644
index 0000000..2a0158c
--- /dev/null
+++ b/portal/js/data/data.html
@@ -0,0 +1,121 @@
+<div class="content-page">
+
+  <bsmodal id="newCollection"
+           title="Create new collection"
+           close="hideModal"
+           closelabel="Cancel"
+           extrabutton="newCollectionDialog"
+           extrabuttonlabel="Create"
+           buttonid="collection"
+           ng-cloak>
+    <fieldset>
+      <div class="control-group">
+        <label for="new-collection-name">Collection Name:</label>
+        <div class="controls">
+          <input type="text" ug-validate required ng-pattern="collectionNameRegex" ng-attr-title="{{collectionNameRegexDescription}}" ng-model="$parent.newCollection.name" name="collection" id="new-collection-name" class="input-xlarge"/>
+          <p class="help-block hide"></p>
+        </div>
+      </div>
+    </fieldset>
+  </bsmodal>
+
+  <section class="row-fluid">
+
+    <div class="span12">
+      <div class="page-filters">
+        <h1 class="title" class="pull-left"><i class="pictogram title">&#128254;</i> Collections</h1>
+      </div>
+    </div>
+
+  </section>
+  <section class="row-fluid">
+    <div class="span3 user-col">
+        <a class="btn btn-primary" id="new-collection-link" ng-click="showModal('newCollection')">New Collection</a>
+        <ul  class="user-list">
+          <li ng-class="queryCollection._type === entity.name ? 'selected' : ''" ng-repeat="entity in collectionList" ng-click="loadCollection('/'+entity.name);">
+            <a id="collection-{{entity.name}}-link" href="javaScript:void(0)">/{{entity.name}} </a>
+          </li>
+        </ul>
+
+  </div>
+
+    <div class="span9 tab-content">
+      <div class="content-page">
+      <form name="dataForm" ng-submit="run();">
+        <fieldset>
+          <div class="control-group">
+            <div class="" data-toggle="buttons-radio">
+              <!--a class="btn" id="button-query-back">&#9664; Back</a-->
+              <!--Added disabled class to change the way button looks but their functionality is as usual -->
+              <label class="control-label" style="display:none"><strong>Method</strong> <a id="query-method-help" href="#" class="help-link">get help</a></label>
+              <input type="radio" id="create-rb" name="query-action" style="margin-top: -2px;" ng-click="selectPOST();" ng-checked="verb=='POST'"> CREATE &nbsp; &nbsp;
+              <input type="radio" id="read-rb" name="query-action" style="margin-top: -2px;" ng-click="selectGET();" ng-checked="verb=='GET'"> READ &nbsp; &nbsp;
+              <input type="radio" id="update-rb" name="query-action" style="margin-top: -2px;" ng-click="selectPUT();" ng-checked="verb=='PUT'"> UPDATE &nbsp; &nbsp;
+              <input type="radio" id="delete-rb" name="query-action" style="margin-top: -2px;" ng-click="selectDELETE();" ng-checked="verb=='DELETE'"> DELETE
+            </div>
+          </div>
+
+          <div class="control-group">
+            <strong>Path </strong>
+            <div class="controls">
+              <input ng-model="data.queryPath" type="text" ug-validate id="pathDataQuery" ng-attr-title="{{pathRegexDescription}}" ng-pattern="pathRegex" class="span6" autocomplete="off" placeholder="ex: /users" required/>
+            </div>
+          </div>
+          <div class="control-group">
+            <a id="back-to-collection" class="outside-link" style="display:none">Back to collection</a>
+          </div>
+          <div class="control-group">
+            <strong>Query</strong>
+            <div class="controls">
+              <input ng-model="data.searchString" type="text" class="span6" autocomplete="off" placeholder="ex: select * where name='fred'"/>
+              <div style="display:none">
+                <a class="btn dropdown-toggle " data-toggle="dropdown">
+                  <span id="query-collections-caret" class="caret"></span>
+                </a>
+                <ul id="query-collections-indexes-list" class="dropdown-menu ">
+                </ul>
+              </div>
+            </div>
+          </div>
+
+
+          <div class="control-group" ng-show="verb=='GET' || verb=='DELETE'">
+            <label class="control-label" for="query-limit"><strong>Limit</strong> <a id="query-limit-help" href="#" ng-show="false" class="help-link">get help</a></label>
+            <div class="controls">
+              <div class="input-append">
+                <input ng-model="data.queryLimit" type="text" class="span5" id="query-limit" placeholder="ex: 10">
+              </div>
+            </div>
+          </div>
+
+          <div class="control-group" style="display:{{queryBodyDisplay}}">
+            <label class="control-label" for="query-source"><strong>JSON Body</strong> <a id="query-json-help" href="#" ng-show="false" class="help-link">get help</a></label>
+            <div class="controls">
+            <textarea ng-model="data.queryBody" id="query-source" class="span6 pull-left" rows="4">
+      { "name":"value" }
+            </textarea>
+              <br>
+            <a class="btn pull-left" ng-click="validateJson();">Validate JSON</a>
+            </div>
+          </div>
+          <div style="clear: both; height: 10px;"></div>
+          <div class="control-group">
+            <input type="submit" ng-disabled="!dataForm.$valid || loading" class="btn btn-primary" id="button-query"  value="{{loading ? loadingText : 'Run Query'}}"/>
+          </div>
+        </fieldset>
+       </form>
+        <div ng-include="display=='generic' ? 'data/display-generic.html' : ''"></div>
+        <div ng-include="display=='users' ? 'data/display-users.html' : ''"></div>
+        <div ng-include="display=='groups' ? 'data/display-groups.html' : ''"></div>
+        <div ng-include="display=='roles' ? 'data/display-roles.html' : ''"></div>
+
+      </div>
+
+      </div>
+    </section>
+
+
+
+
+</div>
+

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/display-generic.html
----------------------------------------------------------------------
diff --git a/portal/js/data/display-generic.html b/portal/js/data/display-generic.html
new file mode 100644
index 0000000..ce4f4f6
--- /dev/null
+++ b/portal/js/data/display-generic.html
@@ -0,0 +1,95 @@
+
+
+<bsmodal id="deleteEntities"
+         title="Are you sure you want to delete the entities(s)?"
+         close="hideModal"
+         closelabel="Cancel"
+         extrabutton="deleteEntitiesDialog"
+         extrabuttonlabel="Delete"
+         buttonid="del-entity"
+         ng-cloak>
+    <fieldset>
+        <div class="control-group">
+        </div>
+    </fieldset>
+</bsmodal>
+
+<span  class="button-strip">
+  <button class="btn btn-primary" ng-disabled="!valueSelected(queryCollection._list) || deleteLoading" ng-click="deleteEntitiesDialog()">{{deleteLoading ? loadingText : 'Delete Entity(s)'}}</button>
+</span>
+<table class="table table-striped collection-list">
+  <thead>
+  <tr class="table-header">
+    <th><input type="checkbox" ng-show="queryCollection._list.length > 0" id="selectAllCheckbox" ng-model="queryBoxesSelected" ng-click="selectAllEntities(queryCollection._list,$parent,'queryBoxesSelected',true)"></th>
+    <th ng-if="hasProperty('name')">Name</th>
+    <th>UUID</th>
+    <th></th>
+  </tr>
+  </thead>
+  <tbody ng-repeat="entity in queryCollection._list">
+  <tr class="zebraRows" >
+    <td>
+      <input
+        type="checkbox"
+        id="entity-{{entity._data.name}}-cb"
+        ng-value="entity._data.uuid"
+        ng-model="entity.checked"
+        >
+    </td>
+    <td ng-if="hasProperty('name')">{{entity._data.name}}</td>
+    <td>{{entity._data.uuid}}</td>
+    <td><a href="javaScript:void(0)" ng-click="entitySelected[$index] = !entitySelected[$index];selectEntity(entity._data.uuid)">{{entitySelected[$index] ? 'Hide' : 'View'}} Details</a></td>
+  </tr>
+  <tr ng-if="entitySelected[$index]">
+    <td colspan="5">
+
+
+      <h4 style="margin: 0 0 20px 0">Entity Detail</h4>
+
+
+      <ul class="formatted-json">
+        <li ng-repeat="(k,v) in entity._data track by $index">
+          <span class="key">{{k}} :</span>
+          <!--todo - doing manual recursion to get this out the door for launch, please fix-->
+          <span ng-switch on="isDeep(v)">
+            <ul ng-switch-when="true">
+              <li ng-repeat="(k2,v2) in v"><span class="key">{{k2}} :</span>
+
+                <span ng-switch on="isDeep(v2)">
+                  <ul ng-switch-when="true">
+                    <li ng-repeat="(k3,v3) in v2"><span class="key">{{k3}} :</span><span class="value">{{v3}}</span></li>
+                  </ul>
+                  <span ng-switch-when="false">
+                    <span class="value">{{v2}}</span>
+                  </span>
+                </span>
+              </li>
+            </ul>
+            <span ng-switch-when="false">
+              <span class="value">{{v}}</span>
+            </span>
+          </span>
+        </li>
+      </ul>
+
+    <div class="control-group">
+      <h4 style="margin: 20px 0 20px 0">Edit Entity</h4>
+      <div class="controls">
+        <textarea ng-model="entity._json" class="span12" rows="12"></textarea>
+        <br>
+        <a class="btn btn-primary toolbar pull-left" ng-click="validateJson();">Validate JSON</a><button type="button" class="btn btn-primary pull-right" id="button-query" ng-click="saveEntity(entity);">Save</button>
+      </div>
+    </div>
+  </td>
+  </tr>
+
+  <tr ng-show="queryCollection._list.length == 0">
+    <td colspan="4">No data found</td>
+  </tr>
+  </tbody>
+</table>
+<div style="padding: 10px 5px 10px 5px">
+  <button class="btn btn-primary toolbar" ng-click="getPrevious()" style="display:{{previous_display}}">< Previous</button>
+  <button class="btn btn-primary toolbar" ng-click="getNext()" style="display:{{next_display}}; float:right;">Next ></button>
+</div>
+

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/display-groups.html
----------------------------------------------------------------------
diff --git a/portal/js/data/display-groups.html b/portal/js/data/display-groups.html
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/display-roles.html
----------------------------------------------------------------------
diff --git a/portal/js/data/display-roles.html b/portal/js/data/display-roles.html
new file mode 100644
index 0000000..91ce3be
--- /dev/null
+++ b/portal/js/data/display-roles.html
@@ -0,0 +1 @@
+roles---------------------------------
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/display-users.html
----------------------------------------------------------------------
diff --git a/portal/js/data/display-users.html b/portal/js/data/display-users.html
new file mode 100644
index 0000000..278e303
--- /dev/null
+++ b/portal/js/data/display-users.html
@@ -0,0 +1,88 @@
+
+<table id="query-response-table" class="table">
+  <tbody>
+  <tr class="zebraRows users-row">
+    <td class="checkboxo">
+      <input type="checkbox" onclick="Usergrid.console.selectAllEntities(this);"></td>
+    <td class="gravatar50-td">&nbsp;</td>
+    <td class="user-details bold-header">Username</td>
+    <td class="user-details bold-header">Display Name</td>
+    <td class="user-details bold-header">UUID</td>
+    <td class="view-details">&nbsp;</td>
+  </tr>
+  <tr class="zebraRows users-row">
+    <td class="checkboxo">
+      <input class="listItem" type="checkbox" name="/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7" value="bf9a95da-d508-11e2-bf44-236d2eee13a7">
+    </td>
+    <td class="gravatar50-td">
+      <img src="http://www.gravatar.com/avatar/01b37aa66496988ca780b3f515bc768e" class="gravatar50">
+    </td>
+    <td class="details">
+      <a onclick="Usergrid.console.getCollection('GET', '/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/'+'bf9a95da-d508-11e2-bf44-236d2eee13a7'); $('#data-explorer').show(); return false;" class="view-details">10</a>
+    </td>
+    <td class="details">      #"&gt;&lt;img src=x onerror=prompt(1);&gt;   </td>
+    <td class="details">     bf9a95da-d508-11e2-bf44-236d2eee13a7   </td>
+    <td class="view-details">
+      <a href="" onclick="$('#query-row-bf9a95da-d508-11e2-bf44-236d2eee13a7').toggle(); $('#data-explorer').show(); return false;" class="view-details">Details</a>
+    </td>
+  </tr>
+  <tr id="query-row-bf9a95da-d508-11e2-bf44-236d2eee13a7" style="display:none">
+    <td colspan="5">
+      <div>
+        <div style="padding-bottom: 10px;">
+          <button type="button" class="btn btn-small query-button active" id="button-query-show-row-JSON" onclick="Usergrid.console.activateQueryRowJSONButton(); $('#query-row-JSON-bf9a95da-d508-11e2-bf44-236d2eee13a7').show(); $('#query-row-content-bf9a95da-d508-11e2-bf44-236d2eee13a7').hide(); return false;">JSON</button>
+          <button type="button" class="btn btn-small query-button disabled" id="button-query-show-row-content" onclick="Usergrid.console.activateQueryRowContentButton();$('#query-row-content-bf9a95da-d508-11e2-bf44-236d2eee13a7').show(); $('#query-row-JSON-bf9a95da-d508-11e2-bf44-236d2eee13a7').hide(); return false;">Content</button>
+        </div>
+        <div id="query-row-JSON-bf9a95da-d508-11e2-bf44-236d2eee13a7">
+              <pre>{
+  "picture": "http://www.gravatar.com/avatar/01b37aa66496988ca780b3f515bc768e",
+  "uuid": "bf9a95da-d508-11e2-bf44-236d2eee13a7",
+  "type": "user",
+  "name": "#"&gt;&lt;img src=x onerror=prompt(1);&gt;",
+  "created": 1371224432557,
+  "modified": 1371851347024,
+  "username": "10",
+  "email": "fdsafdsa@ookfd.com",
+  "activated": "true",
+  "adr": {
+    "addr1": "",
+    "addr2": "",
+    "city": "",
+    "state": "",
+    "zip": "",
+    "country": ""
+  },
+  "metadata": {
+    "path": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7",
+    "sets": {
+      "rolenames": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/rolenames",
+      "permissions": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/permissions"
+    },
+    "collections": {
+      "activities": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/activities",
+      "devices": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/devices",
+      "feed": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/feed",
+      "groups": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/groups",
+      "roles": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/roles",
+      "following": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/following",
+      "followers": "/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/followers"
+    }
+  },
+  "title": "#"&gt;&lt;img src=x onerror=prompt(1);&gt;"
+}</pre>
+        </div>
+        <div id="query-row-content-bf9a95da-d508-11e2-bf44-236d2eee13a7" style="display:none">
+          <table>
+            <tbody>
+            <tr>
+              <td>picture</td>
+              <td>http://www.gravatar.com/avatar/01b37aa66496988ca780b3f515bc768e</td></tr><tr><td>uuid</td><td>bf9a95da-d508-11e2-bf44-236d2eee13a7</td></tr><tr><td>type</td><td>user</td></tr><tr><td>name</td><td>#&amp;quot;&amp;gt;&amp;lt;img src=x onerror=prompt(1);&amp;gt;</td></tr><tr><td>created</td><td>1371224432557</td></tr><tr><td>modified</td><td>1371851347024</td></tr><tr><td>username</td><td>10</td></tr><tr><td>email</td><td>fdsafdsa@ookfd.com</td></tr><tr><td>activated</td><td>true</td></tr><tr><td></td><td style="padding: 0"><table><tbody><tr></tr><tr><td>addr1</td><td></td></tr><tr><td>addr2</td><td></td></tr><tr><td>city</td><td></td></tr><tr><td>state</td><td></td></tr><tr><td>zip</td><td></td></tr><tr><td>country</td><td></td></tr></tbody></table></td></tr><tr><td></td><td style="padding: 0"><table><tbody><tr></tr><tr><td>path</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7</td></tr><tr><td></td><td style="padding:
  0"><table><tbody><tr></tr><tr><td>rolenames</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/rolenames</td></tr><tr><td>permissions</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/permissions</td></tr></tbody></table></td></tr><tr><td></td><td style="padding: 0"><table><tbody><tr></tr><tr><td>activities</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/activities</td></tr><tr><td>devices</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/devices</td></tr><tr><td>feed</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/feed</td></tr><tr><td>groups</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/groups</td></tr><tr><td>roles</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95d
 a-d508-11e2-bf44-236d2eee13a7/roles</td></tr><tr><td>following</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/following</td></tr><tr><td>followers</td><td>/users/8bb9a3fa-d508-11e2-875d-a59031a365e8/following/bf9a95da-d508-11e2-bf44-236d2eee13a7/followers</td></tr></tbody></table></td></tr></tbody></table></td></tr><tr><td>title</td><td>#&amp;quot;&amp;gt;&amp;lt;img src=x onerror=prompt(1);&amp;gt;</td>
+            </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </td>
+  </tr>
+  </tbody>
+</table>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/entity-controller.js
----------------------------------------------------------------------
diff --git a/portal/js/data/entity-controller.js b/portal/js/data/entity-controller.js
new file mode 100644
index 0000000..0d741d6
--- /dev/null
+++ b/portal/js/data/entity-controller.js
@@ -0,0 +1,69 @@
+'use strict'
+
+AppServices.Controllers.controller('EntityCtrl', ['ug', '$scope', '$rootScope', '$location',
+  function (ug, $scope, $rootScope, $location) {
+
+    if (!$rootScope.selectedEntity) {
+      $location.path('/data');
+      return;
+    }
+
+    $scope.entityUUID  = $rootScope.selectedEntity.get('uuid');
+    $scope.entityType  = $rootScope.selectedEntity.get('type');
+    var tempjson  = $rootScope.selectedEntity.get();
+
+    //rip out the system elements.  Stringify first because we don't want to rip them out of the actual object
+    var queryBody = JSON.stringify(tempjson, null, 2);
+    queryBody = JSON.parse(queryBody);
+    delete queryBody.metadata;
+    delete queryBody.uuid;
+    delete queryBody.created;
+    delete queryBody.modified;
+    delete queryBody.type;
+
+    $scope.queryBody = JSON.stringify(queryBody, null, 2);
+
+    $scope.validateJson = function() {
+      var queryBody = $scope.queryBody;
+
+      try {
+        queryBody = JSON.parse(queryBody);
+      } catch (e) {
+        $rootScope.$broadcast('alert', 'error', 'JSON is not valid');
+        return false;
+      }
+
+      queryBody = JSON.stringify(queryBody,null,2);
+
+      $rootScope.$broadcast('alert','success', 'JSON is valid');
+
+      $scope.queryBody = queryBody;
+      return true;
+
+    }
+
+    $scope.saveEntity = function(){
+      if (!$scope.validateJson()) {
+        return false;
+      }
+      var queryBody = $scope.queryBody;
+      queryBody = JSON.parse(queryBody);
+      $rootScope.selectedEntity.set(); //clears out all entities
+      $rootScope.selectedEntity.set(queryBody);
+      $rootScope.selectedEntity.set('type', $scope.entityType);
+      $rootScope.selectedEntity.set('uuid', $scope.entityUUID);
+      $rootScope.selectedEntity.save(function(err, data){
+        if (err) {
+          $rootScope.$broadcast('alert', 'error', 'error: ' + data.error_description);
+        } else {
+          $rootScope.$broadcast('alert', 'success', 'entity saved');
+        }
+
+      });
+    }
+
+
+
+
+
+  }]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/entity.html
----------------------------------------------------------------------
diff --git a/portal/js/data/entity.html b/portal/js/data/entity.html
new file mode 100644
index 0000000..6b9d476
--- /dev/null
+++ b/portal/js/data/entity.html
@@ -0,0 +1,31 @@
+<div class="content-page">
+
+  <h4>Entity Detail</h4>
+  <div class="well">
+    <a href="#!/data" class="outside-link"><< Back to collection</a>
+  </div>
+  <fieldset>
+    <div class="control-group">
+      <strong>Path </strong>
+      <div class="controls">
+        {{entityType}}/{{entityUUID}}
+      </div>
+    </div>
+
+    <div class="control-group">
+      <label class="control-label" for="query-source"><strong>JSON Body</strong></label>
+      <div class="controls">
+        <textarea ng-model="queryBody" class="span6 pull-left" rows="12">{{queryBody}}</textarea>
+        <br>
+        <a class="btn pull-left" ng-click="validateJson();">Validate JSON</a>
+      </div>
+    </div>
+    <div style="clear: both; height: 10px;"></div>
+    <div class="control-group">
+      <button type="button" class="btn btn-primary" id="button-query" ng-click="saveEntity();">Save</button>
+      <!--button type="button" class="btn btn-primary" id="button-query" ng-click="run();">Delete</button-->
+    </div>
+  </fieldset>
+
+</div>
+

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/shell-controller.js
----------------------------------------------------------------------
diff --git a/portal/js/data/shell-controller.js b/portal/js/data/shell-controller.js
new file mode 100644
index 0000000..b06bd9b
--- /dev/null
+++ b/portal/js/data/shell-controller.js
@@ -0,0 +1,9 @@
+'use strict'
+
+AppServices.Controllers.controller('ShellCtrl', ['ug', '$scope', '$rootScope', '$location',
+  function (ug, $scope, $rootScope, $location) {
+
+
+
+
+  }]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/data/shell.html
----------------------------------------------------------------------
diff --git a/portal/js/data/shell.html b/portal/js/data/shell.html
new file mode 100644
index 0000000..b977d81
--- /dev/null
+++ b/portal/js/data/shell.html
@@ -0,0 +1,16 @@
+<div class="content-page">
+  <div class="well">
+    <h2>Interactive Shell</h2>
+    <div style="float:right"><a target="_blank" href="http://apigee.com/docs/usergrid/content/usergrid-admin-portal" class="notifications-links">Learn more in our docs</a></div>
+  </div>
+
+  <div class="console-section-contents">
+    <div id="shell-input-div">
+      <p>   Type "help" to view a list of the available commands.</p><hr>
+      <span>&nbsp;&gt;&gt; </span>
+      <!--textarea id="shell-input" rows="2" autofocus="autofocus"></textarea-->
+    </div>
+    <pre id="shell-output" class="prettyprint lang-js" style="overflow-x: auto; height: 400px;"><span class="pln">                      </span><p><span class="pln">  </span><span class="typ">Response</span><span class="pun">:</span></p><hr><span class="pln">
+    </span></pre>
+  </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/dialogs/balloon-directive.js
----------------------------------------------------------------------
diff --git a/portal/js/dialogs/balloon-directive.js b/portal/js/dialogs/balloon-directive.js
new file mode 100644
index 0000000..1145622
--- /dev/null
+++ b/portal/js/dialogs/balloon-directive.js
@@ -0,0 +1,25 @@
+'use strict';
+
+AppServices.Directives.directive('balloon', ['$window','$timeout', function ($window,$timeout) {
+  return{
+    restrict: 'ECA',
+    scope: '=',
+    template: '' +
+      '<div class="baloon {{direction}}" ng-transclude>' +
+      '</div>',
+    replace: true,
+    transclude: true,
+    link: function linkFn(scope, lElement, attrs) {
+      scope.direction = attrs.direction;
+      var runScroll = true;
+      var windowEl = angular.element($window);
+      windowEl.on('scroll', function() {
+        if(runScroll){
+          lElement.addClass('fade-out');
+          $timeout(function(){lElement.addClass('hide')},1000)
+          runScroll = false;
+        }
+      });
+    }
+  }
+}])
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/dialogs/modal-directive.js
----------------------------------------------------------------------
diff --git a/portal/js/dialogs/modal-directive.js b/portal/js/dialogs/modal-directive.js
new file mode 100644
index 0000000..2b1619c
--- /dev/null
+++ b/portal/js/dialogs/modal-directive.js
@@ -0,0 +1,41 @@
+'use strict';
+
+AppServices.Directives.directive('bsmodal', ["$rootScope", function ($rootScope) {
+  return{
+    restrict: 'ECA',
+    scope: {
+      title: '@title',
+      buttonid:'=buttonid',
+      footertext: '=footertext',
+      closelabel: '=closelabel'
+    },
+    transclude: true,
+    templateUrl: 'dialogs/modal.html',
+    replace: true,
+    link: function linkFn(scope, lElement, attrs, parentCtrl) {
+      scope.title = attrs.title;
+      scope.footertext = attrs.footertext;
+      scope.closelabel = attrs.closelabel;
+      scope.close = attrs.close;
+      scope.extrabutton = attrs.extrabutton;
+      scope.extrabuttonlabel = attrs.extrabuttonlabel;
+      scope.buttonId = attrs.buttonid;
+
+
+      scope.closeDelegate = function(attr){
+        //always call method in parent controller scope
+        scope.$parent[attr](attrs.id,scope)
+      }
+
+      scope.extraDelegate = function(attr){
+        if(scope.dialogForm.$valid){
+          console.log(parentCtrl);
+          //always call method in parent controller scope
+          scope.$parent[attr](attrs.id)
+        }else{
+          $rootScope.$broadcast('alert','error','Please check your form input and resubmit.')
+        }
+      }
+    }
+  }
+}]);

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/dialogs/modal.html
----------------------------------------------------------------------
diff --git a/portal/js/dialogs/modal.html b/portal/js/dialogs/modal.html
new file mode 100644
index 0000000..6dcdaaa
--- /dev/null
+++ b/portal/js/dialogs/modal.html
@@ -0,0 +1,16 @@
+    <div class="modal show fade" tabindex="-1" role="dialog" aria-hidden="true">
+        <form ng-submit="extraDelegate(extrabutton)" name="dialogForm" novalidate>
+
+        <div class="modal-header">
+            <h1 class="title">{{title}}</h1>
+        </div>
+
+        <div class="modal-body" ng-transclude></div>
+        <div class="modal-footer">
+            {{footertext}}
+            <input type="submit" class="btn" id="dialogButton-{{buttonId}}" ng-if="extrabutton" ng-disabled="!dialogForm.$valid" aria-hidden="true" ng-value="extrabuttonlabel"/>
+            <button class="btn cancel pull-left" data-dismiss="modal" aria-hidden="true"
+                    ng-click="closeDelegate(close)">{{closelabel}}
+            </button>
+        </div>
+        </form>    </div>

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/alert-controller.js
----------------------------------------------------------------------
diff --git a/portal/js/global/alert-controller.js b/portal/js/global/alert-controller.js
new file mode 100644
index 0000000..6afb01f
--- /dev/null
+++ b/portal/js/global/alert-controller.js
@@ -0,0 +1,33 @@
+'use strict'
+
+AppServices.Controllers.controller('AlertCtrl', ['$scope', '$rootScope', '$timeout',
+  function ($scope, $rootScope, $timeout) {
+
+    $scope.alertDisplay = 'none';
+    $scope.alerts = [];
+
+    //alert types error,success,warning,info
+
+    $scope.$on('alert', function(event, type, message, permanent) {
+      $scope.addAlert(type, message, permanent);
+    });
+
+    $scope.$on('clear-alerts', function(event, message) {
+      $scope.alerts = [];
+    });
+
+    $scope.addAlert = function(type, message, permanent) {
+      $scope.alertDisplay = 'block';
+      $scope.alerts.push({type:type, msg:message});
+      $scope.applyScope();
+      if(!permanent){
+        $timeout(function(){$scope.alerts.shift()},5000);
+      }
+    };
+
+    $scope.closeAlert = function(index) {
+      $scope.alerts.splice(index, 1);
+    };
+
+
+  }]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/alert-directive.js
----------------------------------------------------------------------
diff --git a/portal/js/global/alert-directive.js b/portal/js/global/alert-directive.js
new file mode 100644
index 0000000..1baff4b
--- /dev/null
+++ b/portal/js/global/alert-directive.js
@@ -0,0 +1,40 @@
+'use strict';
+
+AppServices.Directives.directive('alerti', ["$rootScope","$timeout", function ($rootScope,$timeout) {
+  return{
+    restrict: 'ECA',
+    scope: {
+      type: '=type',
+      closeable: '@closeable',
+      index: '&index'
+    },
+    template: '<div class="alert" ng-class="type && \'alert-\' + type">' +
+              '    <button ng-show="closeable" type="button" class="close" ng-click="closeAlert(index)">&times;</button>' +
+              '    <i ng-if="type === \'warning\'" class="pictogram pull-left" style="font-size:3em;line-height:0.4">&#128165;</i>' +
+      '    <i ng-if="type === \'info\'" class="pictogram pull-left">&#8505;</i>' +
+      '    <i ng-if="type === \'error\'" class="pictogram pull-left">&#9889;</i>' +
+      '    <i ng-if="type === \'success\'" class="pictogram pull-left">&#128077;</i>' +
+      '<div ng-transclude></div>' +
+              '</div>',
+    replace: true,
+    transclude: true,
+    link: function linkFn(scope, lElement, attrs) {
+
+//      if($rootScope.alertClick){
+
+      $timeout(function(){lElement.addClass('fade-out')},4000)
+
+      lElement.click(function(){
+
+        if(attrs.index){
+          scope.$parent.closeAlert(attrs.index);
+        }
+
+      });
+
+//      }
+      //need to do a 10ms timeout to apply animation :(
+      setTimeout(function(){lElement.addClass('alert-animate');},10)
+    }
+  }
+}])
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/app-switcher-directive.js
----------------------------------------------------------------------
diff --git a/portal/js/global/app-switcher-directive.js b/portal/js/global/app-switcher-directive.js
new file mode 100644
index 0000000..56cc77b
--- /dev/null
+++ b/portal/js/global/app-switcher-directive.js
@@ -0,0 +1,35 @@
+'use strict';
+
+AppServices.Directives.directive('appswitcher', ["$rootScope", function ($rootScope) {
+  return{
+    restrict: 'ECA',
+    scope: "=",
+    templateUrl: 'global/appswitcher-template.html',
+    replace: true,
+    transclude: true,
+    link: function linkFn(scope, lElement, attrs) {
+
+      var classNameOpen = 'open';
+      $('ul.nav li.dropdownContainingSubmenu').hover(
+        function () {
+          $(this).addClass(classNameOpen);
+        },
+        function () {
+          $(this).removeClass(classNameOpen);
+        }
+      );
+      $('#globalNav > a').mouseover(globalNavDetail);
+      $('#globalNavDetail').mouseover(globalNavDetail);
+      $('#globalNavSubmenuContainer ul li').mouseover(
+        function () {
+          $('#globalNavDetail > div').removeClass(classNameOpen);
+          $('#' + this.getAttribute('data-globalNavDetail')).addClass(classNameOpen);
+        }
+      );
+      function globalNavDetail() {
+        $('#globalNavDetail > div').removeClass(classNameOpen);
+        $('#globalNavDetailApiPlatform').addClass(classNameOpen);
+      }
+    }
+  }
+}])
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/appswitcher-template.html
----------------------------------------------------------------------
diff --git a/portal/js/global/appswitcher-template.html b/portal/js/global/appswitcher-template.html
new file mode 100644
index 0000000..8bdd6aa
--- /dev/null
+++ b/portal/js/global/appswitcher-template.html
@@ -0,0 +1,34 @@
+<li id="globalNav" class="dropdown dropdownContainingSubmenu active">
+  <a class="dropdown-toggle" data-toggle="dropdown">API Platform<b class="caret"></b></a>
+  <ul class="dropdown-menu pull-right">
+    <li id="globalNavSubmenuContainer">
+      <ul>
+        <li data-globalNavDetail="globalNavDetailApigeeHome"><a target="_blank" href="http://apigee.com">Apigee Home</a></li>
+        <li data-globalNavDetail="globalNavDetailAppServices" class="active"><a target="_blank" href="https://apigee.com/usergrid/">App Services</a></li>
+        <li data-globalNavDetail="globalNavDetailApiPlatform" ><a target="_blank" href="https://enterprise.apigee.com">API Platform</a></li>
+        <li data-globalNavDetail="globalNavDetailApiConsoles"><a target="_blank" href="http://apigee.com/providers">API Consoles</a></li>
+      </ul>
+    </li>
+    <li id="globalNavDetail">
+      <div id="globalNavDetailApigeeHome">
+        <div class="globalNavDetailApigeeLogo"></div>
+        <div class="globalNavDetailDescription">You need apps and apps need APIs. Apigee is the leading API platform for enterprises and developers.</div>
+      </div>
+      <div id="globalNavDetailAppServices">
+        <div class="globalNavDetailSubtitle">For App Developers</div>
+        <div class="globalNavDetailTitle">App Services</div>
+        <div class="globalNavDetailDescription">Build engaging applications, store data, manage application users, and more.</div>
+      </div>
+      <div id="globalNavDetailApiPlatform">
+        <div class="globalNavDetailSubtitle">For API Developers</div>
+        <div class="globalNavDetailTitle">API Platform</div>
+        <div class="globalNavDetailDescription">Create, configure, manage and analyze your APIs and resources.</div>
+      </div>
+      <div id="globalNavDetailApiConsoles">
+        <div class="globalNavDetailSubtitle">For API Developers</div>
+        <div class="globalNavDetailTitle">API Consoles</div>
+        <div class="globalNavDetailDescription">Explore over 100 APIs with the Apigee API Console, or create and embed your own API Console.</div>
+      </div>
+    </li>
+  </ul>
+</li>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/insecure-banner.html
----------------------------------------------------------------------
diff --git a/portal/js/global/insecure-banner.html b/portal/js/global/insecure-banner.html
new file mode 100644
index 0000000..78f90c7
--- /dev/null
+++ b/portal/js/global/insecure-banner.html
@@ -0,0 +1,7 @@
+<div ng-if="securityWarning" ng-cloak class="demo-holder">
+    <div class="alert alert-demo alert-animate">
+        <div class="alert-text">
+            <i class="pictogram">&#9888;</i>Warning: This application has "sandbox" permissions and is not production ready. <a target="_blank" href="http://apigee.com/docs/app-services/content/securing-your-app">Please go to our security documentation to find out more.</a></span>
+        </div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/insecure-banner.js
----------------------------------------------------------------------
diff --git a/portal/js/global/insecure-banner.js b/portal/js/global/insecure-banner.js
new file mode 100644
index 0000000..f4859e5
--- /dev/null
+++ b/portal/js/global/insecure-banner.js
@@ -0,0 +1,46 @@
+
+AppServices.Directives.directive('insecureBanner', ['$rootScope','ug', function ($rootScope,ug) {
+  return{
+    restrict: 'E',
+    transclude: true,
+    templateUrl:'global/insecure-banner.html',
+    link: function linkFn(scope, lElement, attrs) {
+      scope.securityWarning = false;
+
+      scope.$on('roles-received',function(evt,roles){
+        scope.securityWarning = false;
+        if(!roles || !roles._list)
+          return;
+        roles._list.forEach(function(roleHolder){
+          var role = roleHolder._data;
+          if(role.name.toUpperCase()==='GUEST'){
+            roleHolder.getPermissions(function(err, data){
+              if (!err) {
+                if(roleHolder.permissions){
+                  roleHolder.permissions.forEach(function(permission){
+                    if(permission.path.indexOf('/**')>=0){
+                      scope.securityWarning = true;
+                      scope.applyScope();
+                    }
+                  });
+                }
+              }
+            });
+
+          }
+        });
+      });
+
+      var initialized = false;
+      scope.$on('app-initialized',function(){
+        !initialized && ug.getRoles();
+        initialized=true;
+      });
+
+      scope.$on('app-changed',function(){
+        scope.securityWarning = false;
+        ug.getRoles();
+      });
+    }
+  }
+}]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/page-constants.js
----------------------------------------------------------------------
diff --git a/portal/js/global/page-constants.js b/portal/js/global/page-constants.js
new file mode 100644
index 0000000..f77b384
--- /dev/null
+++ b/portal/js/global/page-constants.js
@@ -0,0 +1,11 @@
+'use strict'
+
+/**
+ * Defines application-wide key value pairs
+ */
+
+AppServices.Constants.constant('configuration', {
+
+  ITEMS_URL: 'global/temp.json'
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/page-controller.js
----------------------------------------------------------------------
diff --git a/portal/js/global/page-controller.js b/portal/js/global/page-controller.js
new file mode 100644
index 0000000..81aafcb
--- /dev/null
+++ b/portal/js/global/page-controller.js
@@ -0,0 +1,392 @@
+'use strict'
+
+AppServices.Controllers.controller('PageCtrl',
+  [ 'data',
+    'ug',
+    'utility',
+    '$scope',
+    '$rootScope',
+    '$location',
+    '$routeParams',
+    '$q',
+    '$route',
+    '$log',
+    '$analytics', function (data,
+                     ug,
+                     utility,
+                     $scope,
+                     $rootScope,
+                     $location,
+                     $routeParams,
+                     $q,
+                     $route,
+                     $log,
+                     $analytics) {
+
+  var initScopeVariables = function(){
+    //$rootScope.urls()... will determine which URL should be used for a given environment
+    $scope.loadingText = 'Loading...';
+    $scope.use_sso = false;
+    $scope.newApp = {name: ''};
+    $scope.getPerm = '';
+    $scope.postPerm = '';
+    $scope.putPerm = '';
+    $scope.deletePerm = '';
+    $scope.usersTypeaheadValues = [];
+    $scope.groupsTypeaheadValues = [];
+    $scope.rolesTypeaheadValues = [];
+    $rootScope.sdkActive = false;
+    $rootScope.demoData = false;
+    $scope.queryStringApplied = false;
+    $rootScope.loaded = $rootScope.activeUI = false;
+    for (var key in Usergrid.regex) {
+      $scope[key] = Usergrid.regex[key];
+    }
+
+    $scope.options = Usergrid.options;
+
+    var getQuery = function () {
+      var result = {}, queryString = location.search.slice(1),
+          re = /([^&=]+)=([^&]*)/g, m;
+      while (m = re.exec(queryString)) {
+        result[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
+      }
+      return result;
+    };
+    $scope.queryString = getQuery();
+  };
+
+  initScopeVariables();
+
+  $rootScope.urls = function(){
+    var BASE_URL = '';
+    var DATA_URL = '';
+    var qs = $location.search();
+
+    switch(true){
+      case $location.host() === 'appservices.apigee.com' && location.pathname.indexOf('/dit') >= 0 :
+        //DIT
+        BASE_URL = 'https://accounts.jupiter.apigee.net';
+        DATA_URL = 'http://apigee-internal-prod.jupiter.apigee.net';
+        $scope.use_sso = true;
+        break;
+      case $location.host() === 'appservices.apigee.com' && location.pathname.indexOf('/mars') >= 0  :
+        //staging
+        BASE_URL = 'https://accounts.mars.apigee.net';
+        DATA_URL = 'http://apigee-internal-prod.mars.apigee.net';
+        $scope.use_sso = true;
+        break;
+      case $location.host() === 'appservices.apigee.com' :
+        //enterprise portals
+        DATA_URL = Usergrid.overrideUrl;
+        break;
+      case $location.host() === 'apigee.com' :
+        //prod
+        BASE_URL = 'https://accounts.apigee.com';
+        DATA_URL = 'https://api.usergrid.com';
+        $scope.use_sso = true;
+        break;
+      case $location.host() === 'usergrid.dev':
+        //development
+        DATA_URL = 'https://api.usergrid.com';
+        break;
+      default :
+        DATA_URL = Usergrid.overrideUrl;
+        break;
+    }
+    //override with querystring
+    DATA_URL =  qs.api_url || DATA_URL;
+    $scope.apiUrl = DATA_URL = DATA_URL.lastIndexOf('/') === DATA_URL.length - 1 ? DATA_URL.substring(0,DATA_URL.length-1) : DATA_URL;
+
+    return {
+      DATA_URL: DATA_URL,
+      LOGIN_URL: BASE_URL + '/accounts/sign_in',
+      PROFILE_URL: BASE_URL + '/accounts/my_account',
+      LOGOUT_URL: BASE_URL + '/accounts/sign_out'
+    }
+  }
+
+  //used in users
+  $rootScope.gotoPage = function(path){
+    $location.path(path);
+  }
+
+  var notRegistration = function(){
+    return  "/forgot-password"!==$location.path() && "/register"!==$location.path();
+  };
+
+    //called in ng-init on main index page (first method called at app startup and every main navigation)
+  var verifyUser = function(){
+    //avoid polluting our target route with login path
+     if ($location.path().slice(0, '/login'.length) !== '/login') {
+       $rootScope.currentPath = $location.path();
+       //show loading screen during verify process
+       //      $location.path('/login/loading');
+     }
+
+     //first check to see if there is a token in the query string, if so, save it
+     if ($routeParams.access_token && $routeParams.admin_email && $routeParams.uuid) {
+       ug.set('token', $routeParams.access_token);
+       ug.set('email', $routeParams.admin_email);
+       ug.set('uuid', $routeParams.uuid);
+       var clean_uri = location.protocol + "//" + location.host + location.pathname;
+       window.history.replaceState({}, document.title, clean_uri);
+     }
+
+     $scope.deferredLogin = $q.defer();
+     //use a promise so we can update afterwards in other UI areas
+     //next try to quick login
+     ug.checkAuthentication();
+  };
+
+
+    $scope.profile = function(){
+      if($scope.use_sso){
+        window.location = $rootScope.urls().PROFILE_URL + '?callback=' + encodeURIComponent($location.absUrl());
+      }else{
+        $location.path('/profile')
+      }
+
+    };
+
+    $scope.showModal = function(id){
+      $('#' + id).modal('show')
+    };
+
+    $scope.hideModal = function(id){
+      $('#' + id).modal('hide')
+    };
+
+
+    //adding this here for now
+    //todo - create a controller for usergrid functionality where all UG pages inherit
+    $scope.deleteEntities = function(collection, successBroadcast, errorMessage) {
+      collection.resetEntityPointer();
+      var entitiesToDelete = []
+      while(collection.hasNextEntity()) {
+        var entity = collection.getNextEntity();
+        var checked = entity.checked;
+        if(checked){
+          entitiesToDelete.push(entity);
+        }
+      }
+      var count = 0,success=false;
+      for(var i=0; i<entitiesToDelete.length; i++) {
+        var entity = entitiesToDelete[i];
+        collection.destroyEntity(entity, function(err){
+          count++;
+          if(err){
+            $rootScope.$broadcast('alert', 'error', errorMessage);
+            $rootScope.$broadcast(successBroadcast+'-error',err);
+          }else{
+            success=true;
+          }
+
+          if(count===entitiesToDelete.length){
+            success && $rootScope.$broadcast(successBroadcast);
+            $scope.applyScope();
+          }
+        });
+      }
+    };
+
+    $scope.selectAllEntities = function(list,that,varName,setValue){
+      varName = varName || 'master';
+      var val = that[varName] ;
+      if(setValue == undefined){
+        setValue = true;
+      }
+
+      if(setValue){
+        that[varName] = val = !val;
+      }
+      list.forEach(function(entitiy){
+        entitiy.checked = val;
+      });
+    };
+
+
+    $scope.createPermission = function(type, entity, path, permissions) {
+      //e.g.: "get,post,put:/mypermission"
+//      var path = $scope.path;
+
+      if(path.charAt(0) != '/') {
+        path = '/'+path;
+      }
+      var ops = "";
+      var s = "";
+      if (permissions.getPerm) {
+        ops = "get";
+        s = ",";
+      }
+      if (permissions.postPerm) {
+        ops = ops + s + "post";
+        s = ",";
+      }
+      if (permissions.putPerm) {
+        ops =  ops + s + "put";
+        s = ",";
+      }
+      if (permissions.deletePerm) {
+        ops =  ops + s + "delete";
+        s = ",";
+      }
+      var permission = ops + ":" + path;
+
+      return permission
+    };
+
+    $scope.formatDate = function(date){
+      return new Date(date).toUTCString();
+    };
+
+    $scope.clearCheckbox = function(id){
+      if($('#'+id).attr('checked')){
+        $('#'+id).click();
+      }
+    };
+
+    $scope.removeFirstSlash = function(path){
+      return path.indexOf('/') === 0 ?  path.substring(1,path.length): path;
+    };
+
+    $scope.applyScope = function(cb){
+      cb = typeof cb === 'function' ? cb : function(){};
+      if(!this.$$phase) {
+        return this.$apply(cb);
+      }else{
+        cb();
+      }
+    }
+
+    $scope.valueSelected = function(list){
+      return list &&  (list.some(function(item){return item.checked;}));
+    };
+
+    $scope.$on('users-typeahead-received', function(event, users) {
+      $scope.usersTypeaheadValues = users;
+      if(!$scope.$$phase) {
+        $scope.$apply();
+      }
+    });
+    $scope.$on('groups-typeahead-received', function(event, groups) {
+      $scope.groupsTypeaheadValues = groups;
+      if(!$scope.$$phase) {
+        $scope.$apply();
+      }
+    });
+    $scope.$on('roles-typeahead-received', function(event, roles) {
+      $scope.rolesTypeaheadValues = roles;
+      if(!$scope.$$phase) {
+        $scope.$apply();
+      }
+    });
+
+    $scope.$on('checkAuthentication-success', function () {
+      //all is well - repopulate objects
+      $scope.loaded = true;
+      $rootScope.activeUI = true;
+      $scope.applyScope();
+
+
+      if(!$scope.queryStringApplied){
+        $scope.queryStringApplied = true;
+        setTimeout(function(){
+          //if querystring exists then operate on it.
+          if ($scope.queryString.org) {
+            $rootScope.$broadcast('change-org',$scope.queryString.org);
+          }
+        }, 1000)
+      }
+
+      $scope.deferredLogin && $scope.deferredLogin.resolve();
+      $rootScope.$broadcast('app-initialized');
+
+      //boolean defines is top level ui menus/buttons should be active/shown
+    });
+
+    $scope.$on('checkAuthentication-error', function (args,err, missingData,email) {
+      $scope.loaded = true;
+      if (err && !$scope.use_sso && notRegistration()) {
+        //there was an error on re-auth lite, immediately send to login
+        ug.logout();
+        $location.path('/login');
+        $scope.applyScope();
+      } else {
+        if (missingData && notRegistration()) {
+          if (!email && $scope.use_sso) {
+            window.location = $rootScope.urls().LOGIN_URL + '?callback=' +  encodeURIComponent($location.absUrl().split('?')[0]);
+            return;
+          }
+          ug.reAuthenticate(email);
+        }
+      }
+    });
+
+    $scope.$on('reAuthenticate-success', function (args,err, data, user, organizations, applications) {
+
+      //the user is authenticated
+      $rootScope.$broadcast('loginSuccesful', user, organizations, applications);
+      $rootScope.$emit('loginSuccesful', user, organizations, applications);
+      $rootScope.$broadcast('checkAuthentication-success');
+      $scope.applyScope(function () {
+        $scope.deferredLogin.resolve();
+        $location.path('/org-overview');
+      })
+    });
+
+    $scope.$on('reAuthenticate-error', function () {
+      //user is not authenticated, send to SSO if enabled
+      if ($scope.use_sso) {
+        //go to sso
+        window.location = $rootScope.urls().LOGIN_URL + '?callback=' +  encodeURIComponent($location.absUrl().split('?')[0]);
+      } else {
+        //go to login page
+        if (notRegistration()) {
+          ug.logout();
+          $location.path('/login');
+          $scope.applyScope();
+        }
+      }
+    });
+
+    $scope.$on('loginSuccessful',function(){
+      $rootScope.activeUI = true;
+    });
+
+    $scope.$on('app-changed',function(args,oldVal,newVal,preventReload){
+      if(newVal!==oldVal && !preventReload){
+        $route.reload();
+      }
+    });
+
+    $scope.$on('org-changed',function(args, oldOrg,newOrg){
+      ug.getApplications();
+      $route.reload();
+    });
+
+    $scope.$on('app-settings-received',function(evt,data){
+    });
+
+
+    //verify on every route change
+    $scope.$on('$routeChangeSuccess', function () {
+      //todo possibly do a date check here for token expiry
+      //so we don't call this on every nav change
+      verifyUser();
+      $scope.showDemoBar = $location.path().slice(0,'/performance'.length) === '/performance';
+      if(!$scope.showDemoBar){
+        $rootScope.demoData = false;
+      }
+      $analytics.pageTrack($location.path());
+    });
+    $scope.$on('applications-received', function (event, applications) {
+      $scope.applications = applications;
+      $scope.hasApplications = Object.keys(applications).length > 0;
+    });
+
+    //init app
+    ug.getAppSettings();
+
+  }]);
+

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/page-service.js
----------------------------------------------------------------------
diff --git a/portal/js/global/page-service.js b/portal/js/global/page-service.js
new file mode 100644
index 0000000..d1d08de
--- /dev/null
+++ b/portal/js/global/page-service.js
@@ -0,0 +1,214 @@
+AppServices.Services.factory('data', function (configuration, $q, $http, $resource, $rootScope, $log, $analytics) {
+
+  function reportError(data,config){
+    try {
+      $analytics.eventTrack('error', {
+        category: 'App Services', label: data + ':' + config.url + ':' + (sessionStorage['apigee_uuid'] || 'na')
+      });
+    } catch (e) {
+      console.log(e)
+    }
+  };
+  var getAccessToken = function(){
+    return sessionStorage.getItem('accessToken');
+  };
+
+  return {
+
+    /**
+     * Retrieves page data
+     * @param {string} id the name of the single page item to get (a key in the JSON) can be null.
+     * @param {string} url location of the file/endpoint.
+     * @return {Promise} Resolves to JSON.
+     */
+    get: function (id, url) {
+      var items, deferred;
+
+      deferred = $q.defer();
+
+      $http.get((url || configuration.ITEMS_URL)).
+          success(function (data, status, headers, config) {
+            var result;
+            if (id) {
+              angular.forEach(data, function (obj, index) {
+                if (obj.id === id) {
+                  result = obj;
+                }
+              });
+            } else {
+              result = data;
+            }
+            deferred.resolve(result);
+          }).
+          error(function (data, status, headers, config) {
+            $log.error(data, status, headers, config);
+            reportError(data,config);
+            deferred.reject(data);
+          });
+
+      return deferred.promise;
+    },
+
+//    getLocal: function (url) {
+//      var retdata;
+//      $http.get(url).
+//          success(function (data, status, headers, config) {
+//            retdata = data;
+//          }).
+//          error(function (data, status, headers, config) {
+//          $log.error(data, status, headers, config);
+//          });
+//
+//      return retdata;
+//    },
+
+    /**
+     * Retrieves page data via jsonp
+     * @param {string} url the location of the JSON/RESTful endpoint.
+     * @param {string} successCallback function called on success.
+     */
+//    jsonp: function (url,successCallback) {
+//      var self = this;
+//      $http.jsonp(url).
+//          success(function(data, status, headers, config) {
+//            successCallback(data,status,headers,config);
+//          }).
+//          error(function(data, status, headers, config) {
+//            console.log("ERROR: Could not get data. " + url);
+//          });
+//    },
+
+    jsonp: function (objectType,criteriaId,params,successCallback) {
+      if(!params){
+        params = {};
+      }
+
+      params.demoApp = $rootScope.demoData;
+      params.access_token = getAccessToken();
+      params.callback = 'JSON_CALLBACK';
+
+
+      $rootScope.$broadcast("ajax_loading", objectType);
+
+      $http.jsonp($rootScope.urls().DATA_URL  + '/' + $rootScope.currentOrg + '/' + $rootScope.currentApp + '/apm/' + objectType + '/' + criteriaId,{params:params,headers:{}}).
+        success(function(data, status, headers, config) {
+          successCallback(data,status,headers,config);
+
+          $rootScope.$broadcast("ajax_finished", objectType);
+
+
+        }).
+        error(function(data, status, headers, config) {
+          $log.error("ERROR: Could not get jsonp data. " + $rootScope.urls().DATA_URL + objectType + '/' + criteriaId);
+          reportError(data,config);
+        });
+    },
+
+    jsonp_simple: function (objectType,appId,params) {
+      if(!params){
+        params = {};
+      }
+
+      params.access_token = getAccessToken();
+      params.callback = 'JSON_CALLBACK';
+
+
+      var deferred = $q.defer();
+
+      $http.jsonp($rootScope.urls().DATA_URL  + '/' + $rootScope.currentOrg + '/' + $rootScope.currentApp + '/apm/' + objectType + "/" + appId,{params:params}).
+        success(function(data, status, headers, config) {
+          deferred.resolve(data);
+        }).
+        error(function(data, status, headers, config) {
+          $log.error("ERROR: Could not get jsonp data. " + $rootScope.urls().DATA_URL  + '/' + $rootScope.currentOrg + '/' + $rootScope.currentApp + '/apm/' + objectType + "/" + appId);
+          reportError(data,config);
+          deferred.reject(data);
+        });
+
+      return deferred.promise;
+    },
+
+    jsonp_raw: function (objectType,appId,params) {
+      if(!params){
+        params = {};
+      }
+
+      params.access_token = getAccessToken();
+      params.callback = 'JSON_CALLBACK';
+
+
+      var deferred = $q.defer();
+
+      $http.jsonp($rootScope.urls().DATA_URL  + '/' + $rootScope.currentOrg + '/' + $rootScope.currentApp + '/' + objectType,{params:params}).
+        success(function(data, status, headers, config) {
+          deferred.resolve(data);
+        }).
+        error(function(data, status, headers, config) {
+          $log.error("ERROR: Could not get jsonp data. " + $rootScope.urls().DATA_URL + objectType + '/' + appId);
+          reportError(data,config);
+          deferred.reject(data);
+        });
+
+      return deferred.promise;
+    },
+
+    resource: function(params,isArray) {
+      //temporary url for REST endpoints
+
+      return $resource($rootScope.urls().DATA_URL + '/:orgname/:appname/:username/:endpoint',
+          {
+
+          },
+          {
+            get: {
+              method:'JSONP',
+              isArray: isArray,
+              params: params
+            },
+            login: {
+              method:'GET',
+              url: $rootScope.urls().DATA_URL + '/management/token',
+              isArray: false,
+              params: params
+            },
+            save: {
+              url: $rootScope.urls().DATA_URL + '/' + params.orgname + '/' + params.appname,
+              method:'PUT',
+              isArray: false,
+              params: params
+            }
+          });
+    },
+
+    post: function(url,callback,payload,headers){
+
+      var accessToken = getAccessToken();
+
+      if(payload){
+        payload.access_token = accessToken;
+      }else{
+        payload = {access_token:accessToken}
+      }
+
+      if(!headers){
+        headers = {Bearer:accessToken};
+      }
+
+      $http({method: 'POST', url: url, data: payload, headers: headers}).
+        success(function(data, status, headers, config) {
+          callback(data)
+        }).
+        error(function(data, status, headers, config) {
+          reportError(data,config);
+          callback(data)
+        });
+
+    }
+
+
+
+  }
+
+
+});
+

http://git-wip-us.apache.org/repos/asf/incubator-usergrid/blob/508ef2f7/portal/js/global/temp.json
----------------------------------------------------------------------
diff --git a/portal/js/global/temp.json b/portal/js/global/temp.json
new file mode 100644
index 0000000..f8c400a
--- /dev/null
+++ b/portal/js/global/temp.json
@@ -0,0 +1,18 @@
+[
+  {
+    "id": "appOverview",
+    "top5Crashes": {
+
+      "testdata": 123
+
+    }
+  },
+  {
+    "id": "errorsCrashes",
+    "top5Errors": {
+
+      "testdata": 456
+
+    }
+  }
+]
\ No newline at end of file