From 239dbdbdf5daaa56085fe3f5456f2496c60eddad Mon Sep 17 00:00:00 2001
From: Allen Gilliland <agilliland@gmail.com>
Date: Wed, 11 Mar 2015 09:34:53 -0700
Subject: [PATCH] syncing frontend_client changes from django app.

---
 .../admin/datasets/datasets.controllers.js    | 105 +++++++++--
 .../admin/datasets/partials/dataset_edit.html |  11 +-
 .../admin/datasets/partials/dataset_list.html |  16 +-
 .../admin/datasets/partials/field_detail.html |   2 -
 .../frontend_client/app/card/card.charting.js |   1 +
 .../app/card/card.controllers.js              |   6 +
 resources/frontend_client/app/controllers.js  |  14 +-
 .../frontend_client/app/css/query_builder.css | 159 +++++++++--------
 .../app/react/query_builder.js                | 166 ++++++++++++++++--
 resources/frontend_client/app/services.js     |  61 ++++---
 resources/frontend_client/index.html          |   4 +-
 11 files changed, 393 insertions(+), 152 deletions(-)

diff --git a/resources/frontend_client/app/admin/datasets/datasets.controllers.js b/resources/frontend_client/app/admin/datasets/datasets.controllers.js
index 0ba777f3f04..54035640cc7 100644
--- a/resources/frontend_client/app/admin/datasets/datasets.controllers.js
+++ b/resources/frontend_client/app/admin/datasets/datasets.controllers.js
@@ -40,7 +40,7 @@ AdminDatasetsControllers.controller('AdminDatasetList', ['$scope', 'Metabase', f
 }]);
 
 
-AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams', '$location', 'Metabase', function($scope, $routeParams, $location, Metabase) {
+AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams', '$location', 'Metabase', 'ForeignKey', function($scope, $routeParams, $location, Metabase, ForeignKey) {
 
     $scope.annotations = [];
     $scope.fields = [];
@@ -54,20 +54,13 @@ AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams
         },
     };
 
-    Metabase.table_get({
+
+    Metabase.table_query_metadata({
         'tableId': $routeParams.tableId
     }, function(result) {
         $scope.table = result;
-
-        // get the fields for this table
-        Metabase.table_fields({
-            'tableId': $routeParams.tableId
-        }, function(result) {
-            $scope.fields = result;
-        });
-
-        // table annotations
-
+        $scope.getIdFields();
+        $scope.decorateWithTargets();
     }, function(error) {
         console.log(error);
         if (error.status == 404) {
@@ -75,6 +68,32 @@ AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams
         }
     });
 
+    $scope.getIdFields = function(){
+            // fetch the ID fields
+        Metabase.db_idfields({
+            'dbId': $scope.table.db.id
+        }, function(result) {
+            if (result && !result.error) {
+                $scope.idfields = result;
+                result.forEach(function(field) {
+                    field.displayName = field.table.name + '.' + field.name;
+                });
+            } else {
+                console.log(result);
+            }
+        });
+
+    };
+    
+    $scope.decorateWithTargets = function(){
+        console.log($scope.table);
+        $scope.table.fields.forEach(function(field){
+            if (field.target){
+                field.target_id = field.target.id;
+            }
+        });
+    };
+
     $scope.syncMetadata = function () {
         Metabase.table_sync_metadata({
             'tableId': $routeParams.tableId
@@ -97,17 +116,73 @@ AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams
         }
     };
 
+    $scope.inlineSpecialTypeChange = function(idx){
+        // If we are changing the field from a FK to something else, we should delete any FKs present
+        var field = $scope.table.fields[idx];
+        if (field.target_id && field.special_type != "fk"){
+            // we have something that used to be an FK and is now not an FK
+            // Let's delete its foreign keys
+            var fks = Metabase.field_foreignkeys({'fieldId': field.id}, function(result){
+                fks.forEach(function(fk){
+                    console.log("deleting ", fk);
+                    ForeignKey.delete({'fkID': fks[0].id}, function(result){
+                        console.log("deleted fk");
+                    }, function(error){
+                        console.log("error deleting fk");
+                    });
+                });
+            });
+            // clean up after ourselves
+            field.target = null;
+            field.target_id = null;
+        }
+        // save the field
+        $scope.inlineSaveField(idx);
+    };
+
     $scope.inlineSaveField = function(idx) {
-        if ($scope.fields && $scope.fields[idx]) {
-            Metabase.field_update($scope.fields[idx], function(result) {
+        if ($scope.table.fields && $scope.table.fields[idx]) {
+            Metabase.field_update($scope.table.fields[idx], function(result) {
                 if (result && !result.error) {
-                    $scope.fields[idx] = result;
+                    $scope.table.fields[idx] = result;
                 } else {
                     console.log(result);
                 }
             });
         }
     };
+
+    $scope.inlineChangeFKTarget = function(idx) {
+        // This function notes a change in the target of the target of a foreign key
+        // If there is already a target, we should delete that FK and create a new one
+        // This is meant to be transitional until we add an FK modify function to the API
+        // If there was not a target, we should create a new FK
+        if ($scope.table.fields && $scope.table.fields[idx]) {
+            var field = $scope.table.fields[idx];
+            var new_target_id = field.target_id;
+
+            var fks = Metabase.field_foreignkeys({'fieldId': field.id});
+
+            if(fks.length > 0){
+                // delete this key
+                var relationship_id = 0;
+                 ForeignKey.delete({'fkID': fks[0].id}, function(result){
+                            console.log("Deleted FK");
+                            Metabase.field_addfk({"db": field.table.db.id, "fieldId":field.id,'target_field': new_target_id, "relationship": "Mt1"});
+                            
+                        }, function(error){
+                            console.log('Error deleting key ', error);
+                        });
+            }else{
+
+                Metabase.field_addfk({"db": field.table.db.id, "fieldId":field.id,'target_field': new_target_id, "relationship": "Mt1"});
+            }
+        }
+    };
+
+    $scope.deleteTarget = function(field, target){
+
+    };
 }]);
 
 
diff --git a/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html b/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html
index 31f73e6dd39..5a9efe2f718 100644
--- a/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html
+++ b/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html
@@ -27,7 +27,6 @@
                 <select ng-class="{CustomTypeApplied: table.entity_type }" ng-model="table.entity_type" ng-change="inlineSave()" ng-options="ent_type.id as ent_type.name for ent_type in utils.table_entity_types">
                 </select>
             </label>
-
             <div class="text-grey-4 mt1">
                 <a e-class="full" href="#" ng-class="{EditedEntity: table.description }" editable-text="table.description" onaftersave="inlineSave()">
                     {{table.description}}
@@ -42,7 +41,7 @@
 
 <div class="DragBoundary mt2 border-bottom">
     <ul as-sortable="dragControlListeners" ng-model="fields">
-        <li class="border-top py2 relative text-grey-3" ng-repeat="field in fields" as-sortable-item>
+        <li class="border-top py2 relative text-grey-3" ng-repeat="field in table.fields" as-sortable-item>
             <div class="Drag-handle" title="Reorder" as-sortable-item-handle>
                 <svg width="9" height="36">
                     <rect class="Dragger" fill="url(#dragger)" width="6" height="42">
@@ -63,9 +62,15 @@
                         </select>
                     </label>
                     <label class="Select mx1">
-                        <select ng-class="{CustomTypeApplied: field.special_type }" ng-model="field.special_type" ng-change="inlineSaveField($index)" ng-options="spec_type.id as spec_type.name for spec_type in utils.field_special_types">
+                        <select ng-class="{CustomTypeApplied: field.special_type }" ng-model="field.special_type" ng-change="inlineSpecialTypeChange($index)" ng-options="spec_type.id as spec_type.name for spec_type in utils.field_special_types">
+                        </select>
+                    </label>
+                    <label class="Select mx1" ng-if="field.special_type=='fk'">
+                        <select ng-model="field.target_id" ng-change="inlineChangeFKTarget($index)" ng-options="idf.id as idf.displayName for idf in idfields">
+                        <option value="" disabled selected>Target Field</option>
                         </select>
                     </label>
+
                 </div>
                 <div class="pt1 full">
                     <a e-class="full" href="#" ng-class="{EditedEntity: field.description }" editable-text="field.description" onaftersave="inlineSaveField($index)">
diff --git a/resources/frontend_client/app/admin/datasets/partials/dataset_list.html b/resources/frontend_client/app/admin/datasets/partials/dataset_list.html
index b03f17dd0c4..dd1f4719a2e 100644
--- a/resources/frontend_client/app/admin/datasets/partials/dataset_list.html
+++ b/resources/frontend_client/app/admin/datasets/partials/dataset_list.html
@@ -1,15 +1,19 @@
 <div class="wrapper">
     <div ng-repeat="db in databases">
-        <h2>{{db.name}}</h2>
+        <div class="py2 border-bottom">
+            <div class="float-right">
+                <a class="Button Button--primary" ng-click="syncDatabase(db.id)">Re-Sync Database</a>
+            </div>
+                    <h2>{{db.name}}</h2>
+
+        </div>
         <ul>
-            <li class="py1" ng-repeat="table in db.tables">
-                <a class="EntityListItem" ng-class="{ EditedEntity: table.entity_name, EditedEntityMarker: table.entity_name }" cv-org-href="/admin/datasets/{{table.id}}">
+            <li class="py2 border-bottom" ng-repeat="table in db.tables">
+                <h4><a class="EntityListItem" ng-class="{ EditedEntity: table.entity_name, EditedEntityMarker: table.entity_name }" cv-org-href="/admin/datasets/{{table.id}}">
                     <span class="ModifiedEntity">{{table.entity_name}}</span>
                     <span ng-if="!table.entity_name">{{table.name}}</span>
                 </a>
-            </li>
-            <li>
-                <a class="Button" ng-click="syncDatabase(db.id)">Re-Sync Database</a>
+                </h4>
             </li>
         </ul>
     </div>
diff --git a/resources/frontend_client/app/admin/datasets/partials/field_detail.html b/resources/frontend_client/app/admin/datasets/partials/field_detail.html
index b43cb585d1d..7b9df7ab31d 100644
--- a/resources/frontend_client/app/admin/datasets/partials/field_detail.html
+++ b/resources/frontend_client/app/admin/datasets/partials/field_detail.html
@@ -40,8 +40,6 @@
 				</tbody>
 				</table>
 			</div>
-            <br />
-			<input type="button" value="Add Relationship" class="Button" role="button" field="field" callback="relationshipAdded" cv-field-relationship-modal />
 			<hr />
 			<cv-comments type="field" id="field.id" title="Notes"></cv-comments>
 		</div>
diff --git a/resources/frontend_client/app/card/card.charting.js b/resources/frontend_client/app/card/card.charting.js
index ca2357bbd0f..6d7969ba2d8 100644
--- a/resources/frontend_client/app/card/card.charting.js
+++ b/resources/frontend_client/app/card/card.charting.js
@@ -119,6 +119,7 @@ function getComputedProperty(prop, elementOrId) {
 // computed size properties (drop 'px' and convert string -> Number)
 function getComputedSizeProperty(prop, elementOrId) {
     var val = getComputedProperty(prop, elementOrId);
+    if (!val) return null;
     return Number(val.replace("px", ""));
 }
 var getComputedWidth = _.partial(getComputedSizeProperty, "width");
diff --git a/resources/frontend_client/app/card/card.controllers.js b/resources/frontend_client/app/card/card.controllers.js
index 21151bdff4e..81dea312627 100644
--- a/resources/frontend_client/app/card/card.controllers.js
+++ b/resources/frontend_client/app/card/card.controllers.js
@@ -1450,6 +1450,12 @@ CardControllers.controller('CardDetailNew', [
                             console.log('could not run card!', error);
                         });
                     },
+                    setDisplay: function (type) {
+                        // change the card visualization type and refresh chart settings
+                        $scope.model.card.display = type;
+                        $scope.model.card.visualization_settings = VisualizationSettings.getSettingsForVisualization({}, type);
+                        $scope.model.inform();
+                    },
                 };
                 if ($routeParams.cardId) {
                     // loading up an existing card
diff --git a/resources/frontend_client/app/controllers.js b/resources/frontend_client/app/controllers.js
index d83929cb0db..2dccaf5a961 100644
--- a/resources/frontend_client/app/controllers.js
+++ b/resources/frontend_client/app/controllers.js
@@ -22,6 +22,8 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co
     // current User
     // TODO: can we directly bind to Appstate.model?
     $scope.user = undefined;
+    $scope.userMemberOf = undefined;
+    $scope.userAdminOf = undefined;
     $scope.userIsAdmin = false;
 
     // current Organization
@@ -34,13 +36,15 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co
     $scope.$on("appstate:user", function (event, user) {
         // change in current user
         $scope.user = user;
+        $scope.userMemberOf = user.memberOf();
+        $scope.userAdminOf = user.adminOf();
     });
 
     $scope.$on("appstate:organization", function (event, org) {
         // change in current organization
         $scope.currentOrgSlug = org.slug;
         $scope.currentOrg = org;
-        $scope.userIsAdmin = AppState.userIsAdmin();
+        $scope.userIsAdmin = $scope.user.isAdmin(org.slug);
     });
 
     $scope.$on("appstate:logout", function (event, user) {
@@ -66,14 +70,6 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co
     $scope.refreshCurrentUser = function() {
         AppState.refreshCurrentUser();
     };
-
-    $scope.memberOf = function(){
-        return AppState.memberOf();
-    };
-
-    $scope.adminOf = function(){
-        return AppState.adminOf();
-    };
 }]);
 
 
diff --git a/resources/frontend_client/app/css/query_builder.css b/resources/frontend_client/app/css/query_builder.css
index 96e98571a0e..f1ea1d882ba 100644
--- a/resources/frontend_client/app/css/query_builder.css
+++ b/resources/frontend_client/app/css/query_builder.css
@@ -15,11 +15,23 @@ body {
     background: #fff;
     border-radius: 4px;
     position: absolute;
-    bottom: 2em;
-    right: 2em;
+    bottom: 0.5em;
+    right: 1.5em;
     width: 400px;
 }
 
+.QueryBuilder .dc-chart {
+    padding: 2em;
+}
+
+.QueryBuilder .dc-chart .pie-slice {
+    font-size: 1em;
+}
+
+.QueryWrapper {
+    padding-left: 1.5em;
+    padding-right: 1.5em;
+}
 
 .SaveModal.Modal--showing {
     border: 1px solid #ddd;
@@ -50,11 +62,20 @@ body {
     display: block;
 }
 
+.SaveButton {
+    position: relative;
+    z-index: 10;
+}
+
+.QueryBar {
+    display: inline-block;
+    margin: 0 0 1.2em 0;
+}
+
 .QueryName {
     font-weight: 200;
-    margin-right: 1em;
-    font-size: 1.5em;
-    margin: 2em 0 1.25em;
+    margin: 1em 0;
+    font-size: 1.35em;
 }
 
 .QueryBar .SelectionModule {
@@ -62,11 +83,15 @@ body {
     display: inline-block;
 }
 
+.SelectionModule {
+    color: var(--brand-color);
+}
+
 .SelectionModule .SelectionItems {
     opacity: 0;
     pointer-events: none;
     position: absolute;
-    top: 2em;
+    top: 2.75em;
     border: 1px solid #ddd;
     box-shadow: 0 1px 4px rgba(0, 0, 0, .18);
     border-radius: 4px;
@@ -74,13 +99,12 @@ body {
     z-index: 2;
     left: 0;
     right: 0;
-    overflow: hidden;
+    overflow-y: scroll;
     max-height: 360px;
 }
 
 .SelectionList {
     max-height: 420px;
-    overflow-y: scroll;
 }
 
 .SelectionItems.open {
@@ -89,12 +113,13 @@ body {
 }
 
 .SelectionItem {
-    padding: 0.8em;
-    font-size: 1em;
+    padding: 0.75em;
+    font-size: 0.85em;
     position: relative;
     background: #fff;
     display: flex;
     align-items: center;
+    cursor: pointer;
 }
 
 .SelectionItems {
@@ -102,10 +127,14 @@ body {
 }
 
 .SelectionItem:hover {
-    background: #FBFCFD;
-    transition: background .3s linear;
+    background-color: currentColor;
+}
+
+.SelectionItem:hover .SelectionModule-display {
+    color: #fff;
 }
 
+
 .SelectionItem:before {
     content: '';
     width: 16px;
@@ -113,6 +142,7 @@ body {
     border: 2px solid #ddd;
     border-radius: 99px;
     margin-right: 1em;
+    color: currentColor;
 }
 
 .SelectionItem.selected {
@@ -121,24 +151,16 @@ body {
 }
 
 .SelectionItem.selected:before {
-    color: inherit;
     background-color: currentColor;
     border-color: currentColor;
 }
 
 .QueryBar .SelectionItem:hover:before {
+    color: #fff;
     border-color: currentColor;
     transition: border .3s linear;
 }
 
-.SelectionTitle {
-    display: block;
-}
-
-.SelectionTitle:hover {
-    cursor: pointer;
-}
-
 .QueryBar .SelectionTitle {
     text-align: left;
     font-size: 0.85em;
@@ -146,6 +168,26 @@ body {
     border-radius: 2px;
 }
 
+.FilterSection .SelectionModule,
+.FilterSection .SelectionModule a {
+    color: #A8C28e;
+}
+
+.FilterSection .input {
+    color: #A8C28e;
+    border-color: transparent;
+}
+
+.FilterSection .input:focus {
+    border-color: transparent;
+    border-bottom: 1px solid #A8C28e;
+}
+
+.DimensionList .SelectionModule,
+.DimensionList .SelectionModule a {
+    color: #B9A2CD;
+}
+
 .QueryBar .ActionButton {
     padding: 0.75em 1.25em;
     border: 1px dotted #ddd;
@@ -173,7 +215,7 @@ body {
 }
 
 .SelectionModule-display {
-    color: #444;
+    color: currentColor;
 }
 
 .SelectionModule.selected .SelectionTitle {
@@ -184,6 +226,11 @@ body {
     box-shadow: 0 1px 1px rgba(0, 0, 0, .08);
 }
 
+.SelectionModule.selected .SelectionTitle:hover {
+    box-shadow: 0 1px 1px rgba(0, 0, 0, .18);
+    cursor: pointer;
+}
+
 
 .RemoveTrigger {
     display: inline-block;
@@ -192,9 +239,9 @@ body {
 .ActionBar {
     position: fixed;
     bottom: 0;
-    left: 0;
     right: 0;
     padding: 1em;
+    z-index: 100;
 }
 
 .ActionButton {
@@ -283,35 +330,10 @@ body {
 }
 
 .QueryPicker-group {
-    padding: 2em 0;
     border-bottom: 1px solid #ddd;
     background: #fff;
 }
 
-
-/*
-.with-breakouts:after {
-    content: 'By';
-    display: block;
-    position: absolute;
-    top: 0;
-    right: -1.5em;
-    bottom: 0;
-    color: #999;
-    border: 1px solid #ddd;
-    border-radius: 99px;
-    background: #fff;
-    width: 3em;
-    line-height: 3em;
-    box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
-}
-*/
-
-.SaveButton {
-    position: relative;
-    z-index: 5;
-}
-
 .QueryBar .ActionButton {
     font-size: 0.85em;
 }
@@ -377,6 +399,8 @@ body {
 .Metric-sourceTable:after {
     content: '•';
     display: inline-block;
+    color: #ddd;
+    margin-left: 1em;
 }
 
 .DimensionList .RemoveTrigger {
@@ -391,9 +415,6 @@ body {
     .SelectionTitle {
         padding: 0.75em 1.25em;
     }
-    .QueryPicker-group {
-        padding: 1.2em;
-    }
 }
 
 @media screen and (min-width: 120em) {
@@ -441,25 +462,6 @@ body {
 }
 
 
-.FilterSection .SelectionModule,
-.FilterSection .SelectionModule a {
-    color: #A8C28e;
-}
-
-.FilterSection .input {
-    color: #A8C28e;
-    border-color: transparent;
-}
-
-.FilterSection .input:focus {
-    border-color: transparent;
-    border-bottom: 1px solid #A8C28e;
-}
-
-.DimensionList .SelectionModule,
-.DimensionList .SelectionModule a {
-    color: #B9A2CD;
-}
 
 .FilterTrigger .icon {
     display: flex;
@@ -469,3 +471,20 @@ body {
     overflow: hidden;
     background: #fff;
 }
+
+.TypeControls {
+    position: fixed;
+    bottom: 0;
+    width: 100%;
+    z-index: 100;
+    left: 0;
+    right: 0;
+    text-align: center;
+    margin: 1em 0;
+}
+
+.TypeControls .ActionButton {
+    display: inline-block;
+    font-size: 0.85em;
+    margin: 0 0.5em;
+}
diff --git a/resources/frontend_client/app/react/query_builder.js b/resources/frontend_client/app/react/query_builder.js
index 2e94c411ff2..31bb388b56d 100644
--- a/resources/frontend_client/app/react/query_builder.js
+++ b/resources/frontend_client/app/react/query_builder.js
@@ -4,6 +4,122 @@
 var cx = React.addons.classSet,
     ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
 
+var QueryVisualization = React.createClass({
+    getInitialState: function () {
+        return {
+            type: 'table',
+            chartId: Math.floor((Math.random() * 698754) + 1)
+        }
+    },
+    _changeType: function (type) {
+        this.setState({
+            type: type
+        });
+        this.props.setDisplay(type);
+    },
+    componentDidMount: function () {
+        if (this.state.type !== 'table') {
+            CardRenderer[this.state.type](this.state.chartId, this.props.card, this.props.result.data);
+        }
+    },
+    componentDidUpdate: function () {
+        if (this.state.type !== 'table') {
+            CardRenderer[this.state.type](this.state.chartId, this.props.card, this.props.result.data);
+        }
+    },
+    render: function () {
+        // for table rendering
+        var tableRows,
+            tableHeaders,
+            table;
+        // for chart rendering
+        var titleId,
+            innerId;
+
+        if(this.props.result && this.props.result.data) {
+            if(this.state.type === 'table') {
+                tableRows = this.props.result.data.rows.map(function (row) {
+                    var rowCols = row.map(function (data) {
+                        return (<td>{data.toString()}</td>)
+                    });
+
+                    return (<tr>{rowCols}</tr>);
+                });
+
+                tableHeaders = this.props.result.data.columns.map(function (column) {
+                    return (
+                        <th>{column.toString()}</th>
+                    );
+                });
+
+                table = (
+                    <table className="QueryTable Table">
+                        <thead>
+                            <tr>
+                                {tableHeaders}
+                            </tr>
+                        </thead>
+                        <tbody>
+                            {tableRows}
+                        </tbody>
+                    </table>
+                );
+            } else {
+                titleId = 'card-title--'+this.state.chartId;
+                innerId = 'card-inner--'+this.state.chartId;
+            }
+        }
+
+        var viz;
+        if(this.state.type != 'table') {
+            viz = (
+                <div class="Card--{this.state.type} Card-outer px1" id={this.state.chartId}>
+                    <div id={titleId} class="text-centered"></div>
+                    <div id={innerId} class="card-inner"></div>
+                </div>
+            );
+        } else {
+            viz = (
+                <div className="Table-wrapper">
+                    {table}
+                </div>
+            );
+        }
+
+        var types = [
+            'table',
+            'line',
+            'bar',
+            'pie',
+            'area',
+            'timeseries'
+        ], typeControls = types.map(function (type) {
+            if(this.props.result) {
+                var buttonClasses = cx({
+                    'ActionButton': true,
+                    'ActionButton--primary' : (type == this.state.type)
+                })
+                return (
+                    <a className={buttonClasses} href="#" onClick={this._changeType.bind(null, type)}>{type}</a>
+                );
+            } else {
+                return false;
+            }
+        }.bind(this));
+
+        return (
+            <div>
+                <div className="TypeControls">
+                    <div className="wrapper">
+                        {typeControls}
+                    </div>
+                </div>
+                {viz}
+            </div>
+        );
+    }
+});
+
 var ResultTable = React.createClass({
     render: function () {
         var table,
@@ -109,7 +225,7 @@ var Saver = React.createClass({
                     <div className="ModalContent">
                         <input ref="name" type="text" placeholder="Name" autofocus defaultValue={this.props.name} />
                         <input ref="description" type="text" placeholder="Add a description" defaultValue={this.props.description}/>
-                        <div className="mt4 clearfix">
+                        <div className="mt4 ml2 mr2 clearfix">
                             <span className="text-grey-3 inline-block my1">Privacy:</span>
                             <div className="float-right">
                                 <SelectionModule
@@ -388,7 +504,7 @@ var QueryPicker = React.createClass({
                 }
                 aggregationTargetHtml = (
                     <SelectionModule
-                        placeholder='Lets start with...'
+                        placeholder='field named...'
                         items={this.props.aggregationFieldList[0]}
                         display='1'
                         selectedValue={this.props.query.aggregation[1]}
@@ -497,10 +613,19 @@ var FilterWidget = React.createClass({
         }
         if(this.props.valueFields) {
             if(this.props.valueFields.values) {
+                // do some fixing up of the values so we can display true / false safely
+                var values = this.props.valueFields.values.map(function(value) {
+                    var safeValues = {}
+                    for(var key in value) {
+                        safeValues[key] = value[key].toString()
+                    }
+                    return safeValues
+                })
+
                 valueHtml = (
                     <SelectionModule
                         placeholder="..."
-                        items={this.props.valueFields.values}
+                        items={values}
                         display='name'
                         selectedValue={this.props.value}
                         selectedKey='key'
@@ -522,7 +647,7 @@ var FilterWidget = React.createClass({
             <div className="QueryFilter relative inline-block">
                 <div className="FilterSection">
                     <SelectionModule
-                        placeholder="..."
+                        placeholder="Filter by..."
                         items={this.props.filterFieldList}
                         display='name'
                         selectedValue={this.props.field}
@@ -647,11 +772,22 @@ var QueryBuilder = React.createClass({
             'QueryPicker-group': true
         })
 
+        var saver
+        if(this.props.model.result) {
+            saver = (
+                <Saver
+                    save={this.props.model.save.bind(this.props.model)}
+                    name={this.props.model.card.name}
+                    description={this.props.model.card.description}
+                    hasChanged={this.props.model.hasChanged}
+                />
+            )
+        }
 
         return (
             <div className="full-height">
                     <div className="QueryHeader">
-                        <div className="wrapper">
+                        <div className="QueryWrapper">
                             <QueryHeader
                                 name={this.props.model.card.name}
                                 user={this.props.model.user}
@@ -660,7 +796,7 @@ var QueryBuilder = React.createClass({
                     </div>
                     <div className={queryPickerClasses}>
                         <div>
-                            <div className="wrapper">
+                            <div className="QueryWrapper">
                                 <div className="clearfix">
                                     {runButton}
                                     <QueryPicker
@@ -683,24 +819,22 @@ var QueryBuilder = React.createClass({
                             </div>
                         </div>
                         <div>
-                            <div className="wrapper my2">
+                            <div className="QueryWrapper my2">
                                 {filterHtml}
                             </div>
                         </div>
                     </div>
 
-
-                    <div className="wrapper">
-                        <ResultTable result={this.props.model.result} />
+                    <div className="QueryWrapper mb4">
+                        <QueryVisualization
+                            card={this.props.model.card}
+                            result={this.props.model.result}
+                            setDisplay={this.props.model.setDisplay.bind(this.props.model)}
+                        />
                     </div>
 
                     <div className="ActionBar">
-                            <Saver
-                                save={this.props.model.save.bind(this.props.model)}
-                                name={this.props.model.card.name}
-                                description={this.props.model.card.description}
-                                hasChanged={this.props.model.hasChanged}
-                            />
+                        {saver}
                     </div>
             </div>
         )
diff --git a/resources/frontend_client/app/services.js b/resources/frontend_client/app/services.js
index cd48d8a1481..2194f234dd7 100644
--- a/resources/frontend_client/app/services.js
+++ b/resources/frontend_client/app/services.js
@@ -51,6 +51,36 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati
                 User.current(function(result) {
                     service.model.currentUser = result;
 
+                    // add isMember(orgSlug) method to the object
+                    service.model.currentUser.isMember = function (orgSlug) {
+                        return this.org_perms.some(function (org_perm) {
+                            return org_perm.organization.slug === orgSlug;
+                        });
+                    };
+
+                    // add isAdmin(orgSlug) method to the object
+                    service.model.currentUser.isAdmin = function (orgSlug) {
+                        return this.org_perms.some(function (org_perm) {
+                            return org_perm.organization.slug === orgSlug && org_perm.admin;
+                        }) || this.is_superuser;
+                    };
+
+                    // add memberOf() method to the object enumerating Organizations user is member of
+                    service.model.currentUser.memberOf = function () {
+                        return this.org_perms.map(function (org_perm) {
+                            return org_perm.organization;
+                        });
+                    };
+
+                    // add adminOf() method to the object enumerating Organizations user is admin of
+                    service.model.currentUser.adminOf = function () {
+                        return this.org_perms.filter(function (org_perm) {
+                            return org_perm.admin;
+                        }).map(function (org_perm) {
+                            return org_perm.organization;
+                        });
+                    };
+
                     $rootScope.$broadcast('appstate:user', result);
 
                     deferred.resolve(result);
@@ -62,33 +92,6 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati
                 return deferred.promise;
             },
 
-            userIsAdmin: function() {
-                // Let's also figure out if this user is an admin each time the user or the organization changes
-                return service.model.currentUser.org_perms.some(function(org_perm) {
-                    return org_perm.organization.slug === $routeParams.orgSlug && org_perm.admin;
-                });
-            },
-
-            userIsMember: function() {
-                return service.model.currentUser.org_perms.some(function(org_perm) {
-                    return org_perm.organization.slug === $routeParams.orgSlug;
-                });
-            },
-
-            memberOf: function() {
-                return service.model.currentUser.org_perms.map(function(org_perm) {
-                    return org_perm.organization;
-                });
-            },
-
-            adminOf: function() {
-                return service.model.currentUser.org_perms.filter(function(org_perm) {
-                    return org_perm.admin;
-                }).map(function(org_perm) {
-                    return org_perm.organization;
-                });
-            },
-
             // This function performs whatever state cleanup and next steps are required when a user tries to access
             // something they are not allowed to.
             invalidAccess: function(user, url, message) {
@@ -153,8 +156,8 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati
                     // PERMISSIONS CHECK!!  user must be member of this org to proceed
                     // Making convenience vars so it's easier to scan conditions for correctness
                     var isSuperuser = service.model.currentUser.is_superuser;
-                    var isOrgMember = service.userIsMember();
-                    var isOrgAdmin = service.userIsAdmin();
+                    var isOrgMember = service.model.currentUser.isMember($routeParams.orgSlug);
+                    var isOrgAdmin = service.model.currentUser.isAdmin($routeParams.orgSlug);
                     var onAdminPage = $location.path().indexOf('/' + $routeParams.orgSlug + '/admin') === 0;
 
                     if (!isSuperuser && !isOrgMember) {
diff --git a/resources/frontend_client/index.html b/resources/frontend_client/index.html
index da06a74353e..4934545bc20 100644
--- a/resources/frontend_client/index.html
+++ b/resources/frontend_client/index.html
@@ -23,7 +23,7 @@
               <cv-chevron-down-icon width="8px" height="8px"></cv-chevron-down-icon>
             </span>
             <ul class="Dropdown-content">
-              <li ng-repeat="organization in memberOf()">
+              <li ng-repeat="organization in userMemberOf">
                 <a class="link block py1" href="#" ng-click="changeCurrOrg(organization.slug)">{{organization.name}}</a>
               </li>
             </ul>
@@ -83,7 +83,7 @@
           <div class="border-bottom bg-white py1">
             <div class="wrapper">
               <label class="Select" ng-if="user.is_multi_org">
-                <select ng-change="changeCurrOrg(currentOrgSlug)" ng-model="currentOrgSlug" ng-options="organization.slug as organization.name for organization in adminOf()"></select>
+                <select ng-change="changeCurrOrg(currentOrgSlug)" ng-model="currentOrgSlug" ng-options="organization.slug as organization.name for organization in userAdminOf"></select>
                 </select>
               </label>
               <a cv-org-href="/admin/">
-- 
GitLab