Explorar el Código

[oozie] Initial support for nested dashboards

Cleaned up the UI for the nested dashboard
Separation of the dashboard from the common dashboard code

Conflicts:

	apps/oozie/src/oozie/models2.py
Enrico Berti hace 11 años
padre
commit
5fbbaa1

+ 2 - 0
apps/impala/static/js/impala-dashboard.ko.js

@@ -240,6 +240,8 @@ var Dashboard = function (vm, dashboard) {
 var ImpalaDashboardViewModel = function (query_json, dashboard_json, initial_json) {
     var self = this;
 
+    self.isNested = ko.observable(false);
+
     self.isEditing = ko.observable(true);
     self.toggleEditing = function () {
       self.isEditing(! self.isEditing());

+ 168 - 1
apps/oozie/src/oozie/templates/editor/workflow_editor.mako

@@ -137,7 +137,173 @@ ${ commonheader(_("Workflow Editor"), "Oozie", user) | n,unicode }
 </div>
 
 
-${ dashboard.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">
+##      <div class="row-fluid">
+##        <div class="span12" data-bind="click: function(){$data.addEmptyRow(true)}, css: {'dropTarget': 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': 'dropTargetHighlight', 'greedy': true, 'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"></div>
+##      </div>
+##      <div class="row-fluid">
+##        <div class="span1">W</div>
+##        <div class="span10" data-bind="template: { name: 'row-template', foreach: rows}"></div>
+##        <div class="span1">E</div>
+##      </div>
+##      <div class="row-fluid">
+##        <div class="span12" data-bind="click: function(){$data.addEmptyRow()}, css: {'dropTarget': 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': 'dropTargetHighlight', 'greedy': true, 'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"></div>
+##      </div>
+##    </div>
+
+    <div class="container-fluid" data-bind="visible: $root.isEditing()">
+      <div data-bind="click: function(){$data.addEmptyRow(true)}, css: {'dropTarget': 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': 'dropTargetHighlight', '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() && (rows().length > 0 ||  $root.isNested())">
+      <div data-bind="click: function(){$data.addEmptyRow()}, css: {'dropTarget': 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': 'dropTargetHighlight', '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 && ! $root.isNested">
+    <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-fluid">
+      <div class="span1 pointer">
+        <div data-bind="css: {'dropTarget': true, 'is-editing': $root.isEditing}, droppable: {enabled: $root.isEditing, onDrop: function(){ console.log('dropped'); $data.addEmptyColumn(); } }"></div>
+      </div>
+      <div class="span10">
+
+
+      <div data-bind="visible: columns().length == 0, style: { 'border-bottom': $root.isNested() ? 'none' : '' }, css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing},
+        sortable: { template: 'widget-template', data: widgets, allowDrop: $root.isEditing() && (! $root.isNested() || ($root.isNested() && widgets().length < 1)), isEnabled: $root.isEditing() && (! $root.isNested() || ($root.isNested() && widgets().length < 1)),
+        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 class="container-fluid" data-bind="visible: $root.isNested() && columns().length > 0" style="border: 1px solid #e5e5e5; border-top: none; background-color: #F3F3F3;">
+      <div data-bind="css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing}">
+        <div data-bind="template: { name: 'column-template', foreach: columns}">
+        </div>
+      </div>
+    </div>
+
+
+
+
+      </div>
+      <div class="span1">
+        <div data-bind="css: {'dropTarget': true, 'is-editing': $root.isEditing}, droppable: {enabled: $root.isEditing, onDrop: function(){ console.log('dropped'); $data.addEmptyColumn(); } }"></div>
+      </div>
+    </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: $root.isNested, click: function(){ $data.addEmptyColumn(); }"><i class="fa fa-columns"></i></a>
+##        <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="visible: columns().length == 0, style: { 'border-bottom': $root.isNested() ? 'none' : '' }, css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing},
+##        sortable: { template: 'widget-template', data: widgets, allowDrop: $root.isEditing() && (! $root.isNested() || ($root.isNested() && widgets().length < 1)), isEnabled: $root.isEditing() && (! $root.isNested() || ($root.isNested() && widgets().length < 1)),
+##        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 class="container-fluid" data-bind="visible: $root.isNested() && columns().length > 0" style="border: 1px solid #e5e5e5; border-top: none; background-color: #F3F3F3;">
+##      <div data-bind="css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing}">
+##        <div data-bind="template: { name: 'column-template', foreach: columns}">
+##        </div>
+##      </div>
+##    </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: $root.removeWidget"><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="start-widget">
@@ -578,6 +744,7 @@ ${ dashboard.import_bindings() }
   ko.applyBindings(viewModel);
 
   viewModel.init();
+  fullLayout(viewModel);
 
 
   function columnDropAdditionalHandler(widget) {

+ 17 - 0
apps/oozie/static/css/workflow-editor.css

@@ -292,4 +292,21 @@ em {
 
 #emptyDashboard {
   right: 70px!important;
+}
+
+.dropTarget {
+  background-color: #F6F6F6;
+  min-height: 36px;
+  border: 2px dashed #DDD;
+  text-align: center;
+  padding: 4px;
+}
+
+.ui-sortable.dropTarget {
+  min-height: 36px;
+}
+
+.dropTargetHighlight {
+  min-height: 30px;
+  background-color: #CCC;
 }

+ 55 - 0
apps/oozie/static/js/workflow-editor.ko.js

@@ -14,6 +14,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+ko.bindingHandlers.droppable = {
+  init: function(element, valueAccessor) {
+    var _dropElement = $(element);
+    var _options = valueAccessor();
+
+    if (_options.enabled){
+      var _dropOptions = {
+        hoverClass: 'drag-hover',
+        drop: _options.onDrop
+      };
+      _dropElement.droppable(_dropOptions);
+    }
+  }
+};
+
+
 function magicLayout(vm) {
   loadLayout(vm, vm.initial.layout);
   $(document).trigger("magicLayout");
@@ -38,6 +54,26 @@ function loadLayout(viewModel, json_layout) {
           vm: viewModel
         }));
       });
+      $(json_row.columns).each(function (ccnt, column) {
+        var _irows = [];
+        $(column.rows).each(function (ircnt, json_irow) {
+          var _irow = new Row([], viewModel);
+          $(json_irow.widgets).each(function (iwcnt, iwidget) {
+            _irow.addWidget(new Widget({
+              size:iwidget.size,
+              id: iwidget.id,
+              name: iwidget.name,
+              widgetType: iwidget.widgetType,
+              properties: iwidget.properties,
+              offset: iwidget.offset,
+              loading: true,
+              vm: viewModel
+            }));
+          });
+          _irows.push(_irow);
+        });
+        row.addColumn(new Column(column.size, _irows));
+      });
       _rows.push(row);
     });
     var column = new Column(json_col.size, _rows);
@@ -146,6 +182,8 @@ var Workflow = function (vm, workflow) {
 var WorkflowEditorViewModel = function (layout_json, workflow_json, credentials_json) {
   var self = this;
 
+  self.isNested = ko.observable(true);
+
   self.isEditing = ko.observable(true);
   self.toggleEditing = function () {
     self.isEditing(! self.isEditing());
@@ -184,6 +222,23 @@ var WorkflowEditorViewModel = function (layout_json, workflow_json, credentials_
     return _widget;
   }
 
+  self.removeWidget = function (widget_json) {
+    self.removeWidgetById(widget_json.id());
+  }
+
+  self.removeWidgetById = function (widget_id) {
+    $.each(self.columns(), function (i, col) {
+      $.each(col.rows(), function (j, row) {
+        $.each(row.widgets(), function (z, widget) {
+          if (widget.id() == widget_id){
+            row.widgets.remove(widget);
+          }
+        });
+      });
+    });
+  }
+
+
   self.save = function () {
     $.post("/oozie/editor/workflow/save/", {        
         "layout": ko.mapping.toJSON(self.columns),

+ 1 - 0
apps/search/static/js/search.ko.js

@@ -871,6 +871,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
   var self = this;
 
   self.intervalOptions = ko.observableArray(ko.bindingHandlers.daterangepicker.INTERVAL_OPTIONS);
+  self.isNested = ko.observable(false);
 
   // Models
   self.collection = new Collection(self, collection_json.collection);

+ 13 - 6
desktop/core/src/desktop/templates/common_dashboard.mako

@@ -37,7 +37,7 @@
 <%def name="layout_toolbar()">
 
 <div class="card card-toolbar" data-bind="slideVisible: isEditing">
-  <div style="float: left">
+  <div style="float: left" data-bind="visible: ! $root.isNested">
     <div class="toolbar-label">${_('LAYOUT')}</div>
     <a href="javascript: oneSixthLeftLayout(viewModel)" onmouseover="viewModel.previewColumns('oneSixthLeft')" onmouseout="viewModel.previewColumns('')">
       <div class="layout-container">
@@ -115,19 +115,19 @@
 
 <script type="text/html" id="column-template">
   <div data-bind="css: klass">
-    <div class="container-fluid" data-bind="visible: $root.isEditing()">
+    <div class="container-fluid" data-bind="visible: $root.isEditing() && ! $root.isNested()">
       <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() && rows().length > 0">
+    <div class="container-fluid" data-bind="visible: $root.isEditing() && (rows().length > 0 ||  $root.isNested())">
       <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">
+  <div class="emptyRow" data-bind="visible: widgets().length == 0 && $index() == 0 && $root.isEditing() && $parent.size() > 4 && $parent.rows().length == 1 && ! $root.isNested">
     <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>
@@ -136,18 +136,25 @@
     <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: $root.isNested, click: function(){ $data.addEmptyColumn(); }"><i class="fa fa-columns"></i></a>
         <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,
+    <div data-bind="visible: columns().length == 0, style: { 'border-bottom': $root.isNested() ? 'none' : '' }, css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing},
+        sortable: { template: 'widget-template', data: widgets, allowDrop: $root.isEditing() && (! $root.isNested() || ($root.isNested() && widgets().length < 1)), isEnabled: $root.isEditing() && (! $root.isNested() || ($root.isNested() && widgets().length < 1)),
         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, $data)}}">
     </div>
+    <div class="container-fluid" data-bind="visible: $root.isNested() && columns().length > 0" style="border: 1px solid #e5e5e5; border-top: none; background-color: #F3F3F3;">
+      <div data-bind="css: {'row-fluid': true, 'row-container':true, 'is-editing': $root.isEditing}">
+        <div data-bind="template: { name: 'column-template', foreach: columns}">
+        </div>
+      </div>
+    </div>
   </div>
 </script>
 

+ 36 - 1
desktop/core/static/js/ko.layout.js

@@ -49,14 +49,41 @@ var Column = function (size, rows) {
   };
 }
 
-var Row = function (widgets, vm) {
+var Row = function (widgets, vm, columns) {
   var self = this;
   self.widgets = ko.observableArray(widgets);
+  self.columns = ko.observableArray(columns ? columns : []);
 
   self.addWidget = function (widget) {
     self.widgets.push(widget);
   };
 
+  self.addEmptyColumn = function () {
+    if (self.columns().length == 0){
+      var _col = self.addColumn(null);
+      if (self.widgets().length > 0){
+        var _row = _col.addEmptyRow();
+        self.widgets().forEach(function(widget){
+          _row.addWidget(widget);
+        });
+        self.widgets([]);
+      }
+    }
+    return self.addColumn(null);
+  };
+
+  self.addColumn = function (column) {
+    if (typeof column == "undefined" || column == null) {
+      var _size = Math.max(1, Math.floor(12 / (self.columns().length + 1)));
+      column = new Column(_size, []); // Hacky but needed when a new row is deleted
+      self.columns().forEach(function(col){
+        col.size(_size);
+      });
+    }
+    self.columns.push(column);
+    return column;
+  };
+
   self.move = function (from, to) {
     try {
       vm.columns()[to].addRow(self);
@@ -83,6 +110,14 @@ var Row = function (widgets, vm) {
   }
 
   self.remove = function (col, row) {
+    $.each(self.columns(), function (i, column) {
+      $.each(column.rows(), function (j, row) {
+        $.each(row.widgets(), function (k, widget) {
+          vm.removeWidget(widget);
+        });
+      });
+    });
+
     $.each(self.widgets(), function (i, widget) {
       vm.removeWidget(widget);
     });