Jelajahi Sumber

HUE-2178 [core] Refactor and extract as a lib search layout

Extracted common column, row and widget js
Moved basic toolbar and skeleton layout to common dashboard
Enrico Berti 11 tahun lalu
induk
melakukan
978bacf

+ 22 - 134
apps/search/src/search/templates/search.mako

@@ -19,6 +19,8 @@ from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
 
+<%namespace name="dashboard" file="common_dashboard.mako" />
+
 ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
 
 <script type="text/javascript">
@@ -78,30 +80,9 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
   </form>
 </div>
 
-<div class="card card-toolbar" data-bind="slideVisible: isEditing">
-  <div style="float: left">
-    <div class="toolbar-label">${_('LAYOUT')}</div>
-    <a href="javascript: oneThirdLeftLayout()" onmouseover="viewModel.previewColumns('oneThirdLeft')" onmouseout="viewModel.previewColumns('')">
-      <div class="layout-container">
-        <div class="layout-box" style="width: 24px"></div>
-        <div class="layout-box" style="width: 72px; margin-left: 4px"></div>
-      </div>
-    </a>
-    <a href="javascript: fullLayout()" onmouseover="viewModel.previewColumns('full')" onmouseout="viewModel.previewColumns('')">
-      <div class="layout-container">
-        <div class="layout-box" style="width: 100px;"></div>
-      </div>
-    </a>
-    <a data-bind="visible: columns().length == 0" href="javascript: magicLayout(viewModel)" onmouseover="viewModel.previewColumns('magic')" onmouseout="viewModel.previewColumns('')">
-      <div class="layout-container">
-        <div class="layout-box" style="width: 100px;"><i class="fa fa-magic"></i></div>
-      </div>
-    </a>
-  </div>
-
-  <div style="float: left; margin-left: 20px" data-bind="visible: columns().length > 0">
-    <div class="toolbar-label">${_('WIDGETS')}</div>
-    <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableResultset() },
+<%dashboard:layout_toolbar>
+      <%def name="widgets()">
+        <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableResultset() },
                     draggable: {data: draggableResultset(), isEnabled: availableDraggableResultset,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
                               'stop': function(event, ui){$('.card-body').slideDown('fast'); $root.collection.template.isGridLayout(true); checkResultHighlightingAvailability(); }}}"
@@ -189,112 +170,11 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
                        <i class="hcha hcha-map-chart"></i>
          </a>
    </div>
-  </div>
-  <div class="clearfix"></div>
-</div>
-
-<div id="emptyDashboard" data-bind="fadeVisible: !isEditing() && columns().length == 0">
-  <div style="float:left; padding-top: 90px; margin-right: 20px; text-align: center; width: 260px">${ _('Click on the pencil to get started with your dashboard!') }</div>
-  <img src="/search/static/art/hint_arrow.png" />
-</div>
-
-<div id="emptyDashboardEditing" data-bind="fadeVisible: isEditing() && columns().length == 0 && previewColumns() == ''">
-  <div style="float:right; padding-top: 90px; margin-left: 20px; text-align: center; width: 260px">${ _('Pick an index and Click on a layout to start your dashboard!') }</div>
-  <img src="/search/static/art/hint_arrow_horiz_flipped.png" />
-</div>
-
-
-
-<div data-bind="visible: isEditing() && previewColumns() != '' && columns().length == 0, css:{'with-top-margin': isEditing()}">
-  <div class="container-fluid">
-    <div class="row-fluid" data-bind="visible: previewColumns() == 'oneThirdLeft'">
-      <div class="span3 preview-row"></div>
-      <div class="span9 preview-row"></div>
-    </div>
-    <div class="row-fluid" data-bind="visible: previewColumns() == 'full'">
-      <div class="span12 preview-row">
-      </div>
-    </div>
-    <div class="row-fluid" data-bind="visible: previewColumns() == 'magic'">
-      <div class="span12 preview-row">
-        <div style="text-align: center; color:#EEE; font-size: 180px; margin-top: 80px">
-          <i class="fa fa-magic"></i>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div data-bind="css: {'dashboard': true, 'with-top-margin': isEditing()}">
-  <div class="container-fluid">
-    <div class="row-fluid" data-bind="template: { name: 'column-template', foreach: columns}">
-    </div>
-    <div class="clearfix"></div>
-  </div>
-</div>
+      </%def>
+</%dashboard:layout_toolbar>
 
-<script type="text/html" id="column-template">
-  <div data-bind="css: klass">
-    <div class="container-fluid" data-bind="visible: $root.isEditing">
-      <div data-bind="click: function(){$data.addEmptyRow(true)}, css: {'add-row': true, 'is-editing': $root.isEditing}, sortable: { data: drops, isEnabled: $root.isEditing, 'afterMove': function(event){var widget=event.item; var _r = $data.addEmptyRow(true); _r.addWidget(widget);$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)}); if ($root.collection.getFacetById(widget.id()) == null) { showAddFacetDemiModal(widget); } viewModel.search()}, options: {'placeholder': 'add-row-highlight', 'greedy': true, 'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"></div>
-    </div>
-    <div data-bind="template: { name: 'row-template', foreach: rows}">
-    </div>
-    <div class="container-fluid" data-bind="visible: $root.isEditing">
-      <div data-bind="click: function(){$data.addEmptyRow()}, css: {'add-row': true, 'is-editing': $root.isEditing}, sortable: { data: drops, isEnabled: $root.isEditing, 'afterMove': function(event){var widget=event.item; var _r = $data.addEmptyRow(); _r.addWidget(widget);$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)}); if ($root.collection.getFacetById(widget.id()) == null) { showAddFacetDemiModal(widget); } viewModel.search()}, options: {'placeholder': 'add-row-highlight', 'greedy': true, 'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"></div>
-    </div>
-  </div>
-</script>
-
-<script type="text/html" id="row-template">
-  <div class="emptyRow" data-bind="visible: widgets().length == 0 && $index() == 0 && $root.isEditing() && $parent.size() > 4 && $parent.rows().length == 1">
-    <img src="/search/static/art/hint_arrow_flipped.png" style="float:left; margin-right: 10px"/>
-    <div style="float:left; text-align: center; width: 260px">${_('Drag any of the widgets inside your empty row')}</div>
-    <div class="clearfix"></div>
-  </div>
-  <div class="container-fluid">
-    <div class="row-header" data-bind="visible: $root.isEditing">
-      <span class="muted">${_('Row')}</span>
-      <div style="display: inline; margin-left: 60px">
-        <a href="javascript:void(0)" data-bind="visible: $index()<$parent.rows().length-1, click: function(){moveDown($parent, this)}"><i class="fa fa-chevron-down"></i></a>
-        <a href="javascript:void(0)" data-bind="visible: $index()>0, click: function(){moveUp($parent, this)}"><i class="fa fa-chevron-up"></i></a>
-        <a href="javascript:void(0)" data-bind="visible: $parent.rows().length > 1, click: function(){remove($parent, this)}"><i class="fa fa-times"></i></a>
-      </div>
-    </div>
-    <div data-bind="css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing},
-        sortable: { template: 'widget-template', data: widgets, isEnabled: $root.isEditing,
-        options: {'handle': '.move-widget', 'opacity': 0.7, 'placeholder': 'row-highlight', 'greedy': true,
-            'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});},
-            'helper': function(event){lastWindowScrollPosition = $(window).scrollTop(); $('.card-body').slideUp('fast'); var _par = $('<div>');_par.addClass('card card-widget');var _title = $('<h2>');_title.addClass('card-heading simple');_title.text($(event.toElement).text());_title.appendTo(_par);_par.height(80);_par.width(180);return _par;}},
-            dragged: function(widget){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});showAddFacetDemiModal(widget);viewModel.search()}}">
-    </div>
-  </div>
-</script>
+${ dashboard.layout_skeleton() }
 
-<script type="text/html" id="widget-template">
-  <div data-bind="attr: {'id': 'wdg_'+ id(),}, css: klass">
-    <h2 class="card-heading simple">
-      <span data-bind="visible: $root.isEditing">
-        <a href="javascript:void(0)" class="move-widget"><i class="fa fa-arrows"></i></a>
-        <a href="javascript:void(0)" data-bind="click: compress, visible: size() > 1"><i class="fa fa-step-backward"></i></a>
-        <a href="javascript:void(0)" data-bind="click: expand, visible: size() < 12"><i class="fa fa-step-forward"></i></a>
-        &nbsp;
-      </span>
-      <span data-bind="with: $root.collection.getFacetById(id())">
-        <span data-bind="editable: label, editableOptions: {enabled: $root.isEditing(), placement: 'right'}"></span>
-      </span>
-      <!-- ko ifnot: $root.collection.getFacetById(id()) -->
-        <span data-bind="editable: name, editableOptions: {enabled: $root.isEditing(), placement: 'right'}"></span>
-      <!-- /ko -->
-      <div class="inline pull-right" data-bind="visible: $root.isEditing">
-        <a href="javascript:void(0)" data-bind="click: function(){remove($parent, this)}"><i class="fa fa-times"></i></a>
-      </div>
-    </h2>
-    <div class="card-body" style="padding: 5px;">
-      <div data-bind="template: { name: function() { return widgetType(); }}" class="widget-main-section"></div>
-    </div>
-  </div>
-</script>
 
 <script type="text/html" id="empty-widget">
   ${ _('This is an empty widget.')}
@@ -986,22 +866,18 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
 
 <script src="/static/ext/js/moment-with-langs.min.js" type="text/javascript" charset="utf-8"></script>
 
+${ dashboard.import_layout() }
+
 <script src="/search/static/js/search.utils.js" type="text/javascript" charset="utf-8"></script>
 <script src="/search/static/js/lzstring.min.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/knockout-sortable.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/bootstrap-editable.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/js/ko.editable.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/shortcut.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/mustache.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/jquery/plugins/jquery-ui-1.10.4.draggable-droppable-sortable.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/chosen/chosen.jquery.min.js" type="text/javascript" charset="utf-8"></script>
 
 <script src="/search/static/js/search.ko.js" type="text/javascript" charset="utf-8"></script>
 
-<%namespace name="dashboard" file="common_dashboard.mako" />
-
 ${ dashboard.import_bindings() }
 ${ dashboard.import_charts() }
 
@@ -1312,6 +1188,18 @@ $(document).ready(function () {
     viewModel.collection.template.fieldsSelected(_fields);
   }
 
+  function columnDropAdditionalHandler(widget) {
+    if (viewModel.collection.getFacetById(widget.id()) == null) {
+      showAddFacetDemiModal(widget);
+    }
+    viewModel.search();
+  }
+
+  function widgetDraggedAdditionalHandler(widget) {
+    showAddFacetDemiModal(widget);
+    viewModel.search();
+  }
+
   var selectedWidget = null;
   function showAddFacetDemiModal(widget) {
     if (["resultset-widget", "html-resultset-widget", "filter-widget"].indexOf(widget.widgetType()) == -1) {

+ 0 - 331
apps/search/static/css/search.css

@@ -16,10 +16,6 @@
  limitations under the License.
 */
 
-.container {
-  margin-left: 20px !important;
-}
-
 .dropdown-menu li {
   font-size: 14px;
 }
@@ -175,193 +171,6 @@
   text-align: left;
 }
 
-.dashboard .container-fluid {
-  padding: 6px;
-}
-
-.row-container {
-  width: 100%;
-  min-height: 70px;
-}
-
-.row-container.is-editing {
-  border: 1px solid #e5e5e5;
-}
-
-.ui-sortable {
-  background-color: #F3F3F3;
-  min-height: 100px;
-}
-
-.ui-sortable h2 {
-  padding-left: 10px !important;
-}
-
-.ui-sortable h2 ul {
-  float: left;
-  margin-right: 10px;
-  font-size: 14px;
-}
-
-.ui-sortable-disabled {
-  background-color: #FFF;
-}
-
-.card-column {
-  border: none;
-  min-height: 400px !important;
-}
-
-.card-widget {
-  padding-top: 0;
-  border: 0;
-  min-height: 100px;
-}
-
-.card-widget .card-heading {
-  font-size: 12px !important;
-  font-weight: bold !important;
-  line-height: 24px !important;
-  padding-left: 3px !important;
-  padding-right: 3px !important;
-}
-
-.card-widget .card-body {
-  margin-top: 0;
-  min-height: 40px;
-}
-
-.card-toolbar {
-  margin: 0;
-  padding: 4px;
-  padding-top: 0;
-  top: 70px;
-  position: fixed;
-  width: 100%;
-  z-index: 1000;
-}
-
-.row-header {
-  background-color: #F6F6F6;
-  display: inline;
-  padding: 3px;
-  border: 1px solid #e5e5e5;
-  border-bottom: none;
-}
-
-.row-highlight {
-  background-color: #DDD;
-  min-height: 100px;
-}
-
-#emptyDashboard {
-  position: absolute;
-  right: 204px;
-  top: 80px;
-  color: #666;
-  font-size: 20px;
-  z-index: 1000;
-}
-
-#emptyDashboardEditing {
-  position: absolute;
-  left: 120px;
-  top: 160px;
-  color: #666;
-  font-size: 20px;
-}
-
-.emptyRow {
-  margin-top: 10px;
-  margin-left: 140px;
-  color: #666;
-  font-size: 18px;
-}
-
-.preview-row {
-  background-color: #f9f9f9;
-  min-height: 400px !important;
-  margin-top: 30px;
-}
-
-.toolbar-label {
-  float: left;
-  font-weight: bold;
-  color: #999;
-  padding-left: 8px;
-  padding-top: 24px;
-}
-
-.draggable-widget {
-  width: 60px;
-  text-align: center;
-  float: left;
-  border: 1px solid #CCC;
-  margin-top: 10px;
-  margin-left: 10px;
-  cursor: move;
-}
-
-.draggable-widget.disabled {
-  cursor: default;
-}
-
-.draggable-widget a {
-  font-size: 28px;
-  line-height: 46px;
-}
-
-.draggable-widget.disabled a {
-  cursor: default;
-  color: #CCC;
-}
-
-.layout-container {
-  width: 100px;
-  float: left;
-  margin-top: 10px;
-  margin-left: 10px;
-}
-
-.layout-box {
-  float: left;
-  height: 48px;
-  background-color: #DDD;
-  text-align: center;
-}
-
-.layout-box i {
-  color: #333;
-  font-size: 28px;
-  line-height: 48px;
-}
-
-.layout-container:hover .layout-box {
-  background-color: #CCC;
-}
-
-.with-top-margin {
-  margin-top: 60px;
-}
-
-.ui-sortable .card-heading {
-  -webkit-touch-callout: none;
-  -webkit-user-select: none;
-  -khtml-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-}
-
-.search-bar {
-  padding-top: 6px;
-  padding-bottom: 6px;
-}
-
-.widget-settings-section {
-  display: none;
-}
-
 em {
   font-weight: bold;
   background-color: yellow;
@@ -377,17 +186,6 @@ em {
   font-size: 12px;
 }
 
-.widget-spinner {
-  padding: 10px;
-  font-size: 80px;
-  color: #CCC;
-  text-align: center;
-}
-
-.card {
-  margin: 0;
-}
-
 .badge-left {
   border-radius: 9px 0px 0px 9px;
   padding-right: 5px;
@@ -402,14 +200,6 @@ em {
   cursor: pointer;
 }
 
-.move-widget {
-  cursor: move;
-}
-
-body.modal-open {
-  overflow: auto !important;
-}
-
 .editable-click {
   cursor: pointer;
 }
@@ -455,33 +245,6 @@ body.modal-open {
   border-bottom: 1px solid #338bb8;
 }
 
-.add-row {
-  background-color: #F6F6F6;
-  min-height: 36px;
-  border: 2px dashed #DDD;
-  text-align: center;
-  padding: 4px;
-  cursor: pointer;
-}
-
-.add-row:before {
-  color: #DDD;
-  display: inline-block;
-  font-family: FontAwesome;
-  font-style: normal;
-  font-weight: normal;
-  font-size: 24px;
-  line-height: 1;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  content: "\f055";
-}
-
-.add-row-highlight {
-  min-height: 10px;
-  background-color: #CCC;
-}
-
 .document-details {
   background-color: #F6F6F6;
   padding: 10px;
@@ -517,40 +280,6 @@ body.modal-open {
   border-radius: 3px;
 }
 
-.spinedit-cnt div.spinedit .fa-chevron-up {
-  top: -7px;
-  color: #666;
-}
-
-.spinedit-cnt div.spinedit .fa-chevron-down {
-  top: 7px;
-  left: -4px;
-  color: #666;
-}
-
-.clearable {
-  background: url(/search/static/art/clearField.png) no-repeat right -10px center;
-  border: 1px solid #999;
-  padding: 3px 18px 3px 4px;
-  border-radius: 3px;
-  transition: background 0.4s !important;
-}
-
-@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
-  .clearable {
-    background: url(/search/static/art/clearField@2x.png) no-repeat right -10px center;
-    background-size: 10px 10px;
-  }
-}
-
-.clearable.x {
-  background-position: right 5px center;
-}
-
-.clearable.onX {
-  cursor: pointer !important;
-}
-
 .field-selector {
   display: inline-block;
   margin-left: 3px;
@@ -565,63 +294,3 @@ body.modal-open {
 .field-selector.hoverable:hover {
   background-color: #DDD;
 }
-
-.facet-field-cnt {
-  margin-bottom: 6px;
-}
-
-.facet-field-switch {
-  display: inline;
-  margin-top: 10px;
-}
-
-.facet-field-label {
-  color: #333;
-  font-size: 11px;
-  margin-bottom: 2px;
-  margin-right: 4px;
-  text-transform: uppercase;
-  text-shadow: 1px 1px 0 #fff;
-}
-
-.facet-field-label-fixed-width {
-  width: 90px;
-  text-align: right;
-  padding-right: 8px;
-  display: inline-block;
-}
-
-.simpledaterangepicker .left-margin {
-  margin-left: 6px;
-}
-
-.card-column.span2 .facet-field-label-fixed-width {
-  text-align: left;
-  width: auto;
-}
-
-.card-column.span2 .simpledaterangepicker .facet-field-label-fixed-width {
-  display: block;
-}
-
-.card-column.span2 .simpledaterangepicker .input-prepend {
-  display: block;
-  margin-bottom: 5px;
-}
-
-.card-column.span2 .simpledaterangepicker .left-margin {
-  margin-left: 0;
-}
-
-.card-column.span2 .simpledaterangepicker input[type='text'] {
-  width: 70%;
-}
-
-.card-column.span2 .simpledaterangepicker select {
-  margin-bottom: 5px;
-}
-
-
-.card-widget .input-append, .card-widget .input-prepend, .card-widget select, .card-widget input {
-  margin-bottom: 0;
-}

+ 30 - 174
apps/search/static/js/search.ko.js

@@ -14,182 +14,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
-// Start dashboard lib to move out
-
-var Column = function (size, rows) {
-  var self = this;
-  self.size = ko.observable(size);
-  self.rows = ko.observableArray(rows);
-  self.drops = ko.observableArray(["temp"]);
-  self.klass = ko.computed(function () {
-    return "card card-home card-column span" + self.size();
-  });
-  self.addEmptyRow = function (atBeginning) {
-    return self.addRow(null, atBeginning);
-  };
-  self.addRow = function (row, atBeginning) {
-    if (typeof row == "undefined" || row == null) {
-      row = new Row([]);
-    }
-    if (typeof atBeginning == "undefined" || atBeginning == null) {
-      self.rows.push(row);
-    }
-    else {
-      self.rows.unshift(row);
-    }
-    return row;
-  };
-}
-
-var Row = function (widgets) {
-  var self = this;
-  self.widgets = ko.observableArray(widgets);
-
-  self.addWidget = function (widget) {
-    self.widgets.push(widget);
-  };
-
-  self.move = function (from, to) {
-    try {
-      viewModel.columns()[to].addRow(self);
-      viewModel.columns()[from].rows.remove(self);
-    }
-    catch (exception) {
-    }
-  }
-
-  self.moveDown = function (col, row) {
-    var _i = col.rows().indexOf(row);
-    if (_i < col.rows().length - 1) {
-      var _arr = col.rows();
-      col.rows.splice(_i, 2, _arr[_i + 1], _arr[_i]);
-    }
-  }
-
-  self.moveUp = function (col, row) {
-    var _i = col.rows().indexOf(row);
-    if (_i >= 1) {
-      var _arr = col.rows();
-      col.rows.splice(_i - 1, 2, _arr[_i], _arr[_i - 1]);
-    }
-  }
-
-  self.remove = function (col, row) {
-    $.each(self.widgets(), function(i, widget) {
-      viewModel.removeWidget(widget);
-    });
-    col.rows.remove(row);
-  }
-}
-
-// A widget is generic. It has an id that refer to another object (e.g. facet) with the same id.
-var Widget = function (size, id, name, widgetType, properties, offset, loading) {
-  var self = this;
-  self.size = ko.observable(size).extend({ numeric: 0 });
-
-  self.name = ko.observable(name);
-  self.id = ko.observable(id);
-  self.widgetType = ko.observable(typeof widgetType != "undefined" && widgetType != null ? widgetType : "empty-widget");
-  self.properties = ko.observable(typeof properties != "undefined" && properties != null ? properties : {});
-  self.offset = ko.observable(typeof offset != "undefined" && offset != null ? offset : 0).extend({ numeric: 0 });
-  self.isLoading = ko.observable(typeof loading != "undefined" && loading != null ? loading : false);
-
-
-  self.klass = ko.computed(function () {
-    return "card card-widget span" + self.size() + (self.offset() * 1 > 0 ? " offset" + self.offset() : "");
-  });
-
-  self.expand = function () {
-    self.size(self.size() + 1);
-    $("#wdg_" + self.id()).trigger("resize");
-  }
-
-  self.compress = function () {
-    self.size(self.size() - 1);
-    $("#wdg_" + self.id()).trigger("resize");
-  }
-
-  self.moveLeft = function () {
-    self.offset(self.offset() - 1);
-  }
-
-  self.moveRight = function () {
-    self.offset(self.offset() + 1);
-  }
-
-  self.remove = function (row, widget) {
-    viewModel.removeWidget(widget);
-    row.widgets.remove(widget);
-  }
-};
-
-Widget.prototype.clone = function () {
-  return new Widget(this.size(), UUID(), this.name(), this.widgetType());
-};
-
-function fullLayout() {
-  setLayout([12]);
-}
-
-function oneThirdLeftLayout() {
-  setLayout([2, 10]);
-}
-
 function magicLayout(vm) {
   loadLayout(vm, vm.initial.layout);
   $(document).trigger("magicLayout");
 }
 
-function setLayout(colSizes) {
-  // Save previous widgets
-  var _allRows = [];
-  $(viewModel.columns()).each(function (cnt, col) {
-    var _tRows = [];
-    $(col.rows()).each(function (icnt, row) {
-      if (row.widgets().length > 0) {
-        _tRows.push(row);
-      }
-    });
-    _allRows = _allRows.concat(_tRows);
-  });
-
-  var _cols = [];
-  var _highestCol = {
-    idx: -1,
-    size: -1
-  };
-  $(colSizes).each(function (cnt, size) {
-    _cols.push(new Column(size, []));
-    if (size > _highestCol.size) {
-      _highestCol.idx = cnt;
-      _highestCol.size = size;
-    }
-  });
-  if (_allRows.length > 0 && _highestCol.idx > -1) {
-    _cols[_highestCol.idx].rows(_allRows);
-  }
-
-  $(_cols).each(function (cnt, col) {
-    if (col.rows().length == 0) {
-      col.rows([new Row([])]);
-    }
-  });
-
-  viewModel.columns(_cols);
-
-  $(document).trigger("setLayout");
-}
-
 function loadLayout(viewModel, json_layout) {
   var _columns = [];
 
   $(json_layout).each(function (cnt, json_col) {
     var _rows = [];
     $(json_col.rows).each(function (rcnt, json_row) {
-      var row = new Row();
+      var row = new Row([], viewModel);
       $(json_row.widgets).each(function (wcnt, widget) {
-        row.addWidget(new Widget(widget.size, widget.id, widget.name, widget.widgetType, widget.properties, widget.offset, true));
+        row.addWidget(new Widget({
+          size:widget.size,
+          id: widget.id,
+          name: widget.name,
+          widgetType: widget.widgetType,
+          properties: widget.properties,
+          offset: widget.offset,
+          loading: true,
+          vm: viewModel
+        }));
       });
       _rows.push(row);
     });
@@ -861,16 +708,25 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
   };
   self.isRetrievingResults = ko.observable(false);
 
-  self.draggableHit = ko.observable(new Widget(12, UUID(), "Hit Count", "hit-widget"));
-  self.draggableFacet = ko.observable(new Widget(12, UUID(), "Facet", "facet-widget"));
-  self.draggableResultset = ko.observable(new Widget(12, UUID(), "Grid Results", "resultset-widget"));
-  self.draggableHtmlResultset = ko.observable(new Widget(12, UUID(), "HTML Results", "html-resultset-widget"));
-  self.draggableHistogram = ko.observable(new Widget(12, UUID(), "Histogram", "histogram-widget"));
-  self.draggableBar = ko.observable(new Widget(12, UUID(), "Bar Chart", "bar-widget"));
-  self.draggableMap = ko.observable(new Widget(12, UUID(), "Map", "map-widget"));
-  self.draggableLine = ko.observable(new Widget(12, UUID(), "Line Chart", "line-widget"));
-  self.draggablePie = ko.observable(new Widget(12, UUID(), "Pie Chart", "pie-widget"));
-  self.draggableFilter = ko.observable(new Widget(12, UUID(), "Filter Bar", "filter-widget"));
+  function bareWidgetBuilder(name, type){
+    return new Widget({
+      size: 12,
+      id: UUID(),
+      name: name,
+      widgetType: type
+    });
+  }
+
+  self.draggableHit = ko.observable(bareWidgetBuilder("Hit Count", "hit-widget"));
+  self.draggableFacet = ko.observable(bareWidgetBuilder("Facet", "facet-widget"));
+  self.draggableResultset = ko.observable(bareWidgetBuilder("Grid Results", "resultset-widget"));
+  self.draggableHtmlResultset = ko.observable(bareWidgetBuilder("HTML Results", "html-resultset-widget"));
+  self.draggableHistogram = ko.observable(bareWidgetBuilder("Histogram", "histogram-widget"));
+  self.draggableBar = ko.observable(bareWidgetBuilder("Bar Chart", "bar-widget"));
+  self.draggableMap = ko.observable(bareWidgetBuilder("Map", "map-widget"));
+  self.draggableLine = ko.observable(bareWidgetBuilder("Line Chart", "line-widget"));
+  self.draggablePie = ko.observable(bareWidgetBuilder("Pie Chart", "pie-widget"));
+  self.draggableFilter = ko.observable(bareWidgetBuilder("Filter Bar", "filter-widget"));
 
   self.availableDateFields = ko.computed(function() {
     return $.grep(self.collection.availableFacetFields(), function(field) { return DATE_TYPES.indexOf(field.type()) != -1; });

+ 1 - 11
apps/search/static/js/search.utils.js

@@ -179,14 +179,4 @@ function stripHtml(html) {
   var tmp = document.createElement("DIV");
   tmp.innerHTML = html;
   return tmp.textContent || tmp.innerText;
-}
-
-function s4() {
-  return Math.floor((1 + Math.random()) * 0x10000)
-            .toString(16)
-            .substring(1);
-}
-
-function UUID() {
-   return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
-}
+}

+ 148 - 0
desktop/core/src/desktop/templates/common_dashboard.mako

@@ -25,6 +25,154 @@
   from django.utils.translation import ugettext as _
 %>
 
+<%def name="import_layout()">
+  <link rel="stylesheet" href="/static/css/common_dashboard.css">
+  <script src="/static/js/ko.layout.js" type="text/javascript" charset="utf-8"></script>
+  <script src="/static/ext/js/jquery/plugins/jquery-ui-1.10.4.draggable-droppable-sortable.min.js" type="text/javascript" charset="utf-8"></script>
+  <script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
+  <script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
+  <script src="/static/ext/js/knockout-sortable.min.js" type="text/javascript" charset="utf-8"></script>
+</%def>
+
+<%def name="layout_toolbar()">
+
+<div class="card card-toolbar" data-bind="slideVisible: isEditing">
+  <div style="float: left">
+    <div class="toolbar-label">${_('LAYOUT')}</div>
+    <a href="javascript: oneSixthLeftLayout(viewModel)" onmouseover="viewModel.previewColumns('oneSixthLeft')" onmouseout="viewModel.previewColumns('')">
+      <div class="layout-container">
+        <div class="layout-box" style="width: 24px"></div>
+        <div class="layout-box" style="width: 72px; margin-left: 4px"></div>
+      </div>
+    </a>
+    <a href="javascript: fullLayout(viewModel)" onmouseover="viewModel.previewColumns('full')" onmouseout="viewModel.previewColumns('')">
+      <div class="layout-container">
+        <div class="layout-box" style="width: 100px;"></div>
+      </div>
+    </a>
+    <a data-bind="visible: columns().length == 0" href="javascript: magicLayout(viewModel)" onmouseover="viewModel.previewColumns('magic')" onmouseout="viewModel.previewColumns('')">
+      <div class="layout-container">
+        <div class="layout-box" style="width: 100px;"><i class="fa fa-magic"></i></div>
+      </div>
+    </a>
+  </div>
+  %if hasattr(caller, "widgets"):
+  <div style="float: left; margin-left: 20px" data-bind="visible: columns().length > 0">
+    <div class="toolbar-label">${_('WIDGETS')}</div>
+    ${caller.widgets()}
+  </div>
+  %endif
+  <div class="clearfix"></div>
+</div>
+
+</%def>
+
+<%def name="layout_skeleton()">
+  <div id="emptyDashboard" data-bind="fadeVisible: !isEditing() && columns().length == 0">
+  <div style="float:left; padding-top: 90px; margin-right: 20px; text-align: center; width: 260px">${ _('Click on the pencil to get started with your dashboard!') }</div>
+    <img src="/static/art/hint_arrow.png" />
+  </div>
+
+  <div id="emptyDashboardEditing" data-bind="fadeVisible: isEditing() && columns().length == 0 && previewColumns() == ''">
+    <div style="float:right; padding-top: 90px; margin-left: 20px; text-align: center; width: 260px">${ _('Pick an index and Click on a layout to start your dashboard!') }</div>
+    <img src="/static/art/hint_arrow_horiz_flipped.png" />
+  </div>
+
+
+  <div data-bind="visible: isEditing() && previewColumns() != '' && columns().length == 0, css:{'with-top-margin': isEditing()}">
+  <div class="container-fluid">
+    <div class="row-fluid" data-bind="visible: previewColumns() == 'oneSixthLeft'">
+      <div class="span2 preview-row"></div>
+      <div class="span10 preview-row"></div>
+    </div>
+    <div class="row-fluid" data-bind="visible: previewColumns() == 'full'">
+      <div class="span12 preview-row">
+      </div>
+    </div>
+    <div class="row-fluid" data-bind="visible: previewColumns() == 'magic'">
+      <div class="span12 preview-row">
+        <div style="text-align: center; color:#EEE; font-size: 180px; margin-top: 80px">
+          <i class="fa fa-magic"></i>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div data-bind="css: {'dashboard': true, 'with-top-margin': isEditing()}">
+  <div class="container-fluid">
+    <div class="row-fluid" data-bind="template: { name: 'column-template', foreach: columns}">
+    </div>
+    <div class="clearfix"></div>
+  </div>
+</div>
+
+<script type="text/html" id="column-template">
+  <div data-bind="css: klass">
+    <div class="container-fluid" data-bind="visible: $root.isEditing">
+      <div data-bind="click: function(){$data.addEmptyRow(true)}, css: {'add-row': true, 'is-editing': $root.isEditing}, sortable: { data: drops, isEnabled: $root.isEditing, 'afterMove': function(event){var widget=event.item; var _r = $data.addEmptyRow(true); _r.addWidget(widget);$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)}); columnDropAdditionalHandler(widget)}, options: {'placeholder': 'add-row-highlight', 'greedy': true, 'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"></div>
+    </div>
+    <div data-bind="template: { name: 'row-template', foreach: rows}">
+    </div>
+    <div class="container-fluid" data-bind="visible: $root.isEditing">
+      <div data-bind="click: function(){$data.addEmptyRow()}, css: {'add-row': true, 'is-editing': $root.isEditing}, sortable: { data: drops, isEnabled: $root.isEditing, 'afterMove': function(event){var widget=event.item; var _r = $data.addEmptyRow(); _r.addWidget(widget);$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)}); columnDropAdditionalHandler(widget)}, options: {'placeholder': 'add-row-highlight', 'greedy': true, 'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"></div>
+    </div>
+  </div>
+</script>
+
+<script type="text/html" id="row-template">
+  <div class="emptyRow" data-bind="visible: widgets().length == 0 && $index() == 0 && $root.isEditing() && $parent.size() > 4 && $parent.rows().length == 1">
+    <img src="/static/art/hint_arrow_flipped.png" style="float:left; margin-right: 10px"/>
+    <div style="float:left; text-align: center; width: 260px">${_('Drag any of the widgets inside your empty row')}</div>
+    <div class="clearfix"></div>
+  </div>
+  <div class="container-fluid">
+    <div class="row-header" data-bind="visible: $root.isEditing">
+      <span class="muted">${_('Row')}</span>
+      <div style="display: inline; margin-left: 60px">
+        <a href="javascript:void(0)" data-bind="visible: $index()<$parent.rows().length-1, click: function(){moveDown($parent, this)}"><i class="fa fa-chevron-down"></i></a>
+        <a href="javascript:void(0)" data-bind="visible: $index()>0, click: function(){moveUp($parent, this)}"><i class="fa fa-chevron-up"></i></a>
+        <a href="javascript:void(0)" data-bind="visible: $parent.rows().length > 1, click: function(){remove($parent, this)}"><i class="fa fa-times"></i></a>
+      </div>
+    </div>
+    <div data-bind="css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing},
+        sortable: { template: 'widget-template', data: widgets, isEnabled: $root.isEditing,
+        options: {'handle': '.move-widget', 'opacity': 0.7, 'placeholder': 'row-highlight', 'greedy': true,
+            'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});},
+            'helper': function(event){lastWindowScrollPosition = $(window).scrollTop(); $('.card-body').slideUp('fast'); var _par = $('<div>');_par.addClass('card card-widget');var _title = $('<h2>');_title.addClass('card-heading simple');_title.text($(event.toElement).text());_title.appendTo(_par);_par.height(80);_par.width(180);return _par;}},
+            dragged: function(widget){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});widgetDraggedAdditionalHandler(widget)}}">
+    </div>
+  </div>
+</script>
+
+<script type="text/html" id="widget-template">
+  <div data-bind="attr: {'id': 'wdg_'+ id(),}, css: klass">
+    <h2 class="card-heading simple">
+      <span data-bind="visible: $root.isEditing">
+        <a href="javascript:void(0)" class="move-widget"><i class="fa fa-arrows"></i></a>
+        <a href="javascript:void(0)" data-bind="click: compress, visible: size() > 1"><i class="fa fa-step-backward"></i></a>
+        <a href="javascript:void(0)" data-bind="click: expand, visible: size() < 12"><i class="fa fa-step-forward"></i></a>
+        &nbsp;
+      </span>
+      <!-- ko if: $root.collection && $root.collection.getFacetById(id()) -->
+      <span data-bind="with: $root.collection.getFacetById(id())">
+        <span data-bind="editable: label, editableOptions: {enabled: $root.isEditing(), placement: 'right'}"></span>
+      </span>
+      <!-- /ko -->
+      <!-- ko if: typeof $root.collection == 'undefined' || $root.collection.getFacetById(id()) == null -->
+        <span data-bind="editable: name, editableOptions: {enabled: $root.isEditing(), placement: 'right'}"></span>
+      <!-- /ko -->
+      <div class="inline pull-right" data-bind="visible: $root.isEditing">
+        <a href="javascript:void(0)" data-bind="click: function(){remove($parent, this)}"><i class="fa fa-times"></i></a>
+      </div>
+    </h2>
+    <div class="card-body" style="padding: 5px;">
+      <div data-bind="template: { name: function() { return widgetType(); }}" class="widget-main-section"></div>
+    </div>
+  </div>
+</script>
+</%def>
+
 
 <%def name="import_bindings()">
   <link rel="stylesheet" href="/static/css/freshereditor.css">

+ 0 - 0
apps/search/static/art/clearField.png → desktop/core/static/art/clearField.png


+ 0 - 0
apps/search/static/art/clearField@2x.png → desktop/core/static/art/clearField@2x.png


+ 0 - 0
apps/search/static/art/hint_arrow.png → desktop/core/static/art/hint_arrow.png


+ 0 - 0
apps/search/static/art/hint_arrow_flipped.png → desktop/core/static/art/hint_arrow_flipped.png


+ 0 - 0
apps/search/static/art/hint_arrow_horiz_flipped.png → desktop/core/static/art/hint_arrow_horiz_flipped.png


+ 343 - 0
desktop/core/static/css/common_dashboard.css

@@ -0,0 +1,343 @@
+/*
+ Licensed to Cloudera, Inc. under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  Cloudera, Inc. licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+.dashboard .container-fluid {
+  padding: 6px;
+}
+
+.row-container {
+  width: 100%;
+  min-height: 70px;
+}
+
+.row-container.is-editing {
+  border: 1px solid #e5e5e5;
+}
+
+.ui-sortable {
+  background-color: #F3F3F3;
+  min-height: 100px;
+}
+
+.ui-sortable h2 {
+  padding-left: 10px !important;
+}
+
+.ui-sortable h2 ul {
+  float: left;
+  margin-right: 10px;
+  font-size: 14px;
+}
+
+.ui-sortable-disabled {
+  background-color: #FFF;
+}
+
+.card-column {
+  border: none;
+  min-height: 400px !important;
+}
+
+.card-widget {
+  padding-top: 0;
+  border: 0;
+  min-height: 100px;
+}
+
+.card-widget .card-heading {
+  font-size: 12px !important;
+  font-weight: bold !important;
+  line-height: 24px !important;
+  padding-left: 3px !important;
+  padding-right: 3px !important;
+}
+
+.card-widget .card-body {
+  margin-top: 0;
+  min-height: 40px;
+}
+
+.card-toolbar {
+  margin: 0;
+  padding: 4px;
+  padding-top: 0;
+  top: 70px;
+  position: fixed;
+  width: 100%;
+  z-index: 1000;
+}
+
+.row-header {
+  background-color: #F6F6F6;
+  display: inline;
+  padding: 3px;
+  border: 1px solid #e5e5e5;
+  border-bottom: none;
+}
+
+.row-highlight {
+  background-color: #DDD;
+  min-height: 100px;
+}
+
+#emptyDashboard {
+  position: absolute;
+  right: 204px;
+  top: 80px;
+  color: #666;
+  font-size: 20px;
+  z-index: 1000;
+}
+
+#emptyDashboardEditing {
+  position: absolute;
+  left: 120px;
+  top: 160px;
+  color: #666;
+  font-size: 20px;
+}
+
+.emptyRow {
+  margin-top: 10px;
+  margin-left: 140px;
+  color: #666;
+  font-size: 18px;
+}
+
+.preview-row {
+  background-color: #f9f9f9;
+  min-height: 400px !important;
+  margin-top: 30px;
+}
+
+.toolbar-label {
+  float: left;
+  font-weight: bold;
+  color: #999;
+  padding-left: 8px;
+  padding-top: 24px;
+}
+
+.draggable-widget {
+  width: 60px;
+  text-align: center;
+  float: left;
+  border: 1px solid #CCC;
+  margin-top: 10px;
+  margin-left: 10px;
+  cursor: move;
+}
+
+.draggable-widget.disabled {
+  cursor: default;
+}
+
+.draggable-widget a {
+  font-size: 28px;
+  line-height: 46px;
+}
+
+.draggable-widget.disabled a {
+  cursor: default;
+  color: #CCC;
+}
+
+.layout-container {
+  width: 100px;
+  float: left;
+  margin-top: 10px;
+  margin-left: 10px;
+}
+
+.layout-box {
+  float: left;
+  height: 48px;
+  background-color: #DDD;
+  text-align: center;
+}
+
+.layout-box i {
+  color: #333;
+  font-size: 28px;
+  line-height: 48px;
+}
+
+.layout-container:hover .layout-box {
+  background-color: #CCC;
+}
+
+.with-top-margin {
+  margin-top: 60px;
+}
+
+.ui-sortable .card-heading {
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.search-bar {
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.widget-settings-section {
+  display: none;
+}
+
+.widget-spinner {
+  padding: 10px;
+  font-size: 80px;
+  color: #CCC;
+  text-align: center;
+}
+
+.card {
+  margin: 0;
+}
+
+.move-widget {
+  cursor: move;
+}
+
+body.modal-open {
+  overflow: auto !important;
+}
+
+.add-row {
+  background-color: #F6F6F6;
+  min-height: 36px;
+  border: 2px dashed #DDD;
+  text-align: center;
+  padding: 4px;
+  cursor: pointer;
+}
+
+.add-row:before {
+  color: #DDD;
+  display: inline-block;
+  font-family: FontAwesome;
+  font-style: normal;
+  font-weight: normal;
+  font-size: 24px;
+  line-height: 1;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  content: "\f055";
+}
+
+.add-row-highlight {
+  min-height: 10px;
+  background-color: #CCC;
+}
+
+.spinedit-cnt div.spinedit .fa-chevron-up {
+  top: -7px;
+  color: #666;
+}
+
+.spinedit-cnt div.spinedit .fa-chevron-down {
+  top: 7px;
+  left: -4px;
+  color: #666;
+}
+
+.clearable {
+  background: url(/static/art/clearField.png) no-repeat right -10px center;
+  border: 1px solid #999;
+  padding: 3px 18px 3px 4px;
+  border-radius: 3px;
+  transition: background 0.4s !important;
+}
+
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+  .clearable {
+    background: url(/static/art/clearField@2x.png) no-repeat right -10px center;
+    background-size: 10px 10px;
+  }
+}
+
+.clearable.x {
+  background-position: right 5px center;
+}
+
+.clearable.onX {
+  cursor: pointer !important;
+}
+
+.facet-field-cnt {
+  margin-bottom: 6px;
+}
+
+.facet-field-switch {
+  display: inline;
+  margin-top: 10px;
+}
+
+.facet-field-label {
+  color: #333;
+  font-size: 11px;
+  margin-bottom: 2px;
+  margin-right: 4px;
+  text-transform: uppercase;
+  text-shadow: 1px 1px 0 #fff;
+}
+
+.facet-field-label-fixed-width {
+  width: 90px;
+  text-align: right;
+  padding-right: 8px;
+  display: inline-block;
+}
+
+.simpledaterangepicker .left-margin {
+  margin-left: 6px;
+}
+
+.card-column.span2 .facet-field-label-fixed-width {
+  text-align: left;
+  width: auto;
+}
+
+.card-column.span2 .simpledaterangepicker .facet-field-label-fixed-width {
+  display: block;
+}
+
+.card-column.span2 .simpledaterangepicker .input-prepend {
+  display: block;
+  margin-bottom: 5px;
+}
+
+.card-column.span2 .simpledaterangepicker .left-margin {
+  margin-left: 0;
+}
+
+.card-column.span2 .simpledaterangepicker input[type='text'] {
+  width: 70%;
+}
+
+.card-column.span2 .simpledaterangepicker select {
+  margin-bottom: 5px;
+}
+
+.card-widget .input-append, .card-widget .input-prepend, .card-widget select, .card-widget input {
+  margin-bottom: 0;
+}

+ 217 - 0
desktop/core/static/js/ko.layout.js

@@ -0,0 +1,217 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+function s4() {
+  return Math.floor((1 + Math.random()) * 0x10000)
+      .toString(16)
+      .substring(1);
+}
+
+function UUID() {
+  return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
+}
+
+var Column = function (size, rows) {
+  var self = this;
+  self.size = ko.observable(size);
+  self.rows = ko.observableArray(rows);
+  self.drops = ko.observableArray(["temp"]);
+  self.klass = ko.computed(function () {
+    return "card card-home card-column span" + self.size();
+  });
+  self.addEmptyRow = function (atBeginning) {
+    return self.addRow(null, atBeginning);
+  };
+  self.addRow = function (row, atBeginning) {
+    if (typeof row == "undefined" || row == null) {
+      row = new Row([]);
+    }
+    if (typeof atBeginning == "undefined" || atBeginning == null) {
+      self.rows.push(row);
+    }
+    else {
+      self.rows.unshift(row);
+    }
+    return row;
+  };
+}
+
+var Row = function (widgets, vm) {
+  var self = this;
+  self.widgets = ko.observableArray(widgets);
+
+  self.addWidget = function (widget) {
+    self.widgets.push(widget);
+  };
+
+  self.move = function (from, to) {
+    try {
+      vm.columns()[to].addRow(self);
+      vm.columns()[from].rows.remove(self);
+    }
+    catch (exception) {
+    }
+  }
+
+  self.moveDown = function (col, row) {
+    var _i = col.rows().indexOf(row);
+    if (_i < col.rows().length - 1) {
+      var _arr = col.rows();
+      col.rows.splice(_i, 2, _arr[_i + 1], _arr[_i]);
+    }
+  }
+
+  self.moveUp = function (col, row) {
+    var _i = col.rows().indexOf(row);
+    if (_i >= 1) {
+      var _arr = col.rows();
+      col.rows.splice(_i - 1, 2, _arr[_i], _arr[_i - 1]);
+    }
+  }
+
+  self.remove = function (col, row) {
+    $.each(self.widgets(), function (i, widget) {
+      vm.removeWidget(widget);
+    });
+    col.rows.remove(row);
+  }
+}
+
+
+// A widget is generic. It has an id that refer to another object (e.g. facet) with the same id.
+var Widget = function (params) {
+  var self = this;
+  self.size = ko.observable(params.size).extend({ numeric: 0 });
+
+  self.name = ko.observable(params.name);
+  self.id = ko.observable(params.id);
+  self.widgetType = ko.observable(typeof params.widgetType != "undefined" && params.widgetType != null ? params.widgetType : "empty-widget");
+  self.properties = ko.observable(typeof params.properties != "undefined" && params.properties != null ? params.properties : {});
+  self.offset = ko.observable(typeof params.offset != "undefined" && params.offset != null ? params.offset : 0).extend({ numeric: 0 });
+  self.isLoading = ko.observable(typeof params.loading != "undefined" && params.loading != null ? params.loading : false);
+
+
+  self.klass = ko.computed(function () {
+    return "card card-widget span" + self.size() + (self.offset() * 1 > 0 ? " offset" + self.offset() : "");
+  });
+
+  self.expand = function () {
+    self.size(self.size() + 1);
+    $("#wdg_" + self.id()).trigger("resize");
+  }
+
+  self.compress = function () {
+    self.size(self.size() - 1);
+    $("#wdg_" + self.id()).trigger("resize");
+  }
+
+  self.moveLeft = function () {
+    self.offset(self.offset() - 1);
+  }
+
+  self.moveRight = function () {
+    self.offset(self.offset() + 1);
+  }
+
+  self.remove = function (row, widget) {
+    if (params.vm != null) {
+      params.vm.removeWidget(widget);
+    }
+    row.widgets.remove(widget);
+  }
+};
+
+Widget.prototype.clone = function () {
+  return new Widget({
+    size: this.size(),
+    id: UUID(),
+    name: this.name(),
+    widgetType: this.widgetType()
+  });
+};
+
+function fullLayout(vm) {
+  setLayout([12], vm);
+}
+
+function oneSixthLeftLayout(vm) {
+  setLayout([2, 10], vm);
+}
+
+function oneFourthLeftLayout(vm) {
+  setLayout([3, 9], vm);
+}
+
+function oneThirdLeftLayout(vm) {
+  setLayout([4, 8], vm);
+}
+
+function halfHalfLayout(vm) {
+  setLayout([6, 6], vm);
+}
+
+function oneThirdRightLayout(vm) {
+  setLayout([8, 4], vm);
+}
+
+function oneFourthRightLayout(vm) {
+  setLayout([9, 3], vm);
+}
+
+function oneSixthRightLayout(vm) {
+  setLayout([10, 2], vm);
+}
+
+
+function setLayout(colSizes, vm) {
+  // Save previous widgets
+  var _allRows = [];
+  $(vm.columns()).each(function (cnt, col) {
+    var _tRows = [];
+    $(col.rows()).each(function (icnt, row) {
+      if (row.widgets().length > 0) {
+        _tRows.push(row);
+      }
+    });
+    _allRows = _allRows.concat(_tRows);
+  });
+
+  var _cols = [];
+  var _highestCol = {
+    idx: -1,
+    size: -1
+  };
+  $(colSizes).each(function (cnt, size) {
+    _cols.push(new Column(size, []));
+    if (size > _highestCol.size) {
+      _highestCol.idx = cnt;
+      _highestCol.size = size;
+    }
+  });
+  if (_allRows.length > 0 && _highestCol.idx > -1) {
+    _cols[_highestCol.idx].rows(_allRows);
+  }
+
+  $(_cols).each(function (cnt, col) {
+    if (col.rows().length == 0) {
+      col.rows([new Row([], vm)]);
+    }
+  });
+
+  vm.columns(_cols);
+
+  $(document).trigger("setLayout");
+}