Pārlūkot izejas kodu

HUE-2186 [search] Bulk edit collection templates

Moved inline style from admin_collections.mako and replaced with classes.

.hueCheckbox: Reverted to display: block after noticing subtle behavioral changes
.coll-heading: new class added to address vertical alignment of table heading in conjuction with copy/delete links pulled-right

Copied methods for toggling checkboxes from indexer.

Removed console logging used for debugging

toggleCollectionSelect(): updated references from collection.name to collection.id to enable toggling of individual checkboxes

collections.js: addded methods to handle bulk actions (markManyForDeletion, deleteCollections, copyCollections); will modify remove unused methods later
admin_collections.mako: removed existing templates and added table with check boxes and single, bulk-action buttons/links (copy, delete); removed inline styles
search_controller.py: added bulk action methods (delete_collections, copy_collections)
Paul McCaughtry 11 gadi atpakaļ
vecāks
revīzija
1699f0a

+ 50 - 0
apps/search/src/search/search_controller.py

@@ -40,6 +40,20 @@ class SearchController(object):
     # TODO perms
     return Collection.objects.filter(enabled=True)
 
+  def delete_collections(self, collection_ids):
+    if isinstance(collection_ids, basestring):
+      collection_ids = collection_ids.split();
+
+    for x in collection_ids:
+      id = x.id
+      try:
+        Collection.objects.get(id=x.id).delete()
+      except Exception, e:
+        LOG.warn('Error deleting collection: %s' % e)
+        id = -1
+
+      return id
+
   def delete_collection(self, collection_id):
     id = collection_id
     try:
@@ -50,6 +64,42 @@ class SearchController(object):
 
     return id
 
+  def copy_collections(self, collection_ids):
+    if isinstance(collection_ids, basestring):
+      collection_ids = collection_ids.split();
+      
+    for x in collection_ids:
+      id = -1
+
+      try:
+        copy = Collection.objects.get(id=x)
+        copy.label += _(' (Copy)')
+        copy.id = copy.pk = None
+        copy.save()
+
+        facets = copy.facets
+        facets.id = None
+        facets.save()
+        copy.facets = facets
+
+        result = copy.result
+        result.id = None
+        result.save()
+        copy.result = result
+
+        sorting = copy.sorting
+        sorting.id = None
+        sorting.save()
+        copy.sorting = sorting
+
+        copy.save()
+
+        id = copy.id
+      except Exception, e:
+        LOG.warn('Error copying collection: %s' % e)
+
+      return id
+
   def copy_collection(self, collection_id):
     id = -1
 

+ 79 - 65
apps/search/src/search/templates/admin_collections.mako

@@ -38,55 +38,67 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
 
 <div class="container-fluid">
   <div class="card">
-  <%actionbar:render>
-    <%def name="search()">
-      <input type="text" placeholder="${_('Filter dashboards...')}" class="input-xlarge search-query" id="filterInput" data-bind="visible: collections().length > 0 && !isLoading()">
-      &nbsp;
-      &nbsp;
-      <a data-bind="visible: collections().length > 0 && !isLoading()" class="btn" href="${ url('search:new_search') }" title="${ _('Create a new dashboard') }"><i class="fa fa-plus-circle"></i> ${ _('Dashboard') }</a>
-    </%def>
-
-    <%def name="creation()">
-    </%def>
-  </%actionbar:render>
-
-  <div class="row-fluid" data-bind="visible: collections().length == 0 && !isLoading()">
-    <div class="span10 offset1 center importBtn" style="cursor: pointer">
-      <i class="fa fa-plus-circle waiting"></i>
-      <h1 class="emptyMessage">
-        ${ _('There are currently no dashboards defined.') }<br/>
-        <a class="btn importBtn" href="${ url('search:new_search') }">
-          <i class="fa fa-plus-circle"></i> ${ _('Dashboard') }
-        </a>
-      </h1>
-    </div>
-  </div>
-  <div class="row-fluid" data-bind="visible: isLoading()">
-    <div class="span10 offset1 center">
-      <i class="fa fa-spinner fa-spin" style="font-size: 60px; color: #DDD"></i>
+    <%actionbar:render>
+      <%def name="search()">
+        <input type="text" placeholder="${_('Filter dashboards...')}" class="input-xlarge search-query" id="filterInput" data-bind="visible: collections().length > 0 && !isLoading()">
+        &nbsp;
+        &nbsp;
+        <a data-bind="visible: collections().length > 0 && !isLoading()" class="btn" href="${ url('search:new_search') }" title="${ _('Create a new dashboard') }"><i class="fa fa-plus-circle"></i> ${ _('Dashboard') }</a>
+      </%def>
+
+      <%def name="creation()">
+      </%def>
+    </%actionbar:render>
+
+    <div class="row-fluid" data-bind="visible: collections().length == 0 && !isLoading()">
+      <div class="span10 offset1 center importBtn pointer">
+        <i class="fa fa-plus-circle waiting"></i>
+        <h1 class="emptyMessage">
+          ${ _('There are currently no dashboards defined.') }<br/>
+          <a class="btn importBtn" href="${ url('search:new_search') }">
+            <i class="fa fa-plus-circle"></i> ${ _('Dashboard') }
+          </a>
+        </h1>
+      </div>
     </div>
-  </div>
-  <div class="row-fluid">
-    <div class="span12">
-      <p>
-      <ul id="collections" data-bind="template: {name: 'collectionTemplate', foreach: filteredCollections}">
-      </ul>
-      </p>
+    
+    <div class="row-fluid" data-bind="visible: isLoading()">
+      <div class="span10 offset1 center">
+        <i class="fa fa-spinner fa-spin spinner"></i>
+      </div>
     </div>
-  </div>
 
-  <script id="collectionTemplate" type="text/html">
-    <li class="collectionRow" data-bind="click: $root.editCollection" title="${ _('Click to edit') }">
-      <div class="pull-right" style="margin-top: 10px;margin-right: 10px; cursor: pointer">
-        <a data-bind="click: $root.copyCollection, clickBubble: false"><i class="fa fa-files-o"></i> ${_('Copy')}</a> &nbsp;
-        <a data-bind="click: $root.markForDeletion, clickBubble: false"><i class="fa fa-times"></i> ${_('Delete')}</a>
-      </div>
-      <h4><img src="/search/static/art/icon_search_48.png" class="app-icon"/> <span data-bind="text: label"></span></h4>
-    </li>
-  </script>
+    <div class="row-fluid" data-bind="visible: collections().length > 0 && !isLoading()">
+      <table class="table table-condensed">
+        <thead>
+          <tr>
+            <th>
+              <span data-bind="click: toggleSelectAll, css: {'fa-check': !ko.utils.arrayFilter(filteredCollections(), function(collection) {return !collection.selected()}).length}" class="hueCheckbox fa"></span>
+            </th>
+            <th width="100%">
+              <span class="coll-heading">${_('Name')}</span>
+              <span class="pull-right collection-actions">
+                <a data-bind="click: $root.copyCollections, clickBubble: false"><i class="fa fa-files-o"></i> ${_('Copy')}</a> &nbsp;&nbsp;
+                <a data-bind="click: $root.markManyForDeletion, clickBubble: false"><i class="fa fa-times"></i> ${_('Delete')}</a>
+              </span>
+            </th>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: filteredCollections">
+          <tr class="pointer">
+            <td data-bind="click: $root.toggleCollectionSelect.bind($root), clickBubble: false">
+              <span data-bind="css: {'fa-check': $root.filteredCollections()[$index()].selected()}" class="hueCheckbox fa"></span>
+            </td>
+            <td data-bind="text: label, click: $root.editCollection" title="${ _('Click to edit') }"></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
   </div>
 </div>
 
+<!-- PAUL - REMOVE before committing -->
+<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
 
 <script id="importableTemplate" type="text/html">
   <tr>
@@ -111,27 +123,25 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
   </div>
 </div>
 
-
-<style type="text/css">
-  #collections {
-    list-style-type: none;
-    margin: 0;
-    padding: 0;
-    width: 100%;
-  }
-
-  .placeholder {
-    height: 40px;
-    background-color: #F5F5F5;
-    border: 1px solid #E3E3E3;
-  }
-</style>
+<div id="deleteManyModal" class="modal hide fade">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${_('Confirm Delete')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>${_('Are you sure you want to delete this collection?')}</p>
+  </div>
+  <div class="modal-footer">
+    <a class="btn" data-dismiss="modal">${_('No')}</a>
+    <a id="deleteModalBtn" class="btn btn-danger disable-feedback" data-bind="click: deleteCollections">${_('Yes')}</a>
+  </div>
+</div>
 
 <script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/search/static/js/collections.ko.js" type="text/javascript" charset="utf-8"></script>
 
-<script type="text/javascript">
-
+<script>
+//(function () {
   var appProperties = {
     labels: [],
     listCollectionsUrl: "${ url("search:admin_collections") }?format=json",
@@ -146,14 +156,13 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
     viewModel.updateCollections();
 
     var orderedCores;
-    serializeList();
 
-    function serializeList() {
+    (function serializeList() {
       orderedCores = [];
       $("#collections li").each(function () {
         orderedCores.push($(this).data("collection"));
       });
-    }
+    }());
 
     var filter = -1;
     $("#filterInput").on("keyup", function () {
@@ -163,7 +172,7 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
       }, 300);
     });
 
-    $("#deleteModal").modal({
+    $("#deleteModal, #deleteManyModal").modal({
       show: false
     });
 
@@ -174,7 +183,7 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
     });
 
     $(document).on("collectionDeleted", function () {
-      $("#deleteModal").modal("hide");
+      $("#deleteModal, #deleteManyModal").modal("hide");
       $("#deleteModalBtn").button("reset");
       $(document).trigger("info", "${ _("Collection deleted successfully.") }");
     });
@@ -187,7 +196,12 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
       $("#deleteModal").modal('show');
     });
 
+    $(document).on("confirmDeleteMany", function () {
+      $("#deleteManyModal").modal('show');
+    });
+
   });
+//}());
 </script>
 
 ${ commonfooter(messages) | n,unicode }

+ 48 - 3
apps/search/static/js/collections.ko.js

@@ -20,7 +20,7 @@ var Importable = function (importable) {
   self.name = ko.observable(importable.name);
   self.selected = ko.observable(false);
   self.handleSelect = function (row, e) {
-    this.selected(!this.selected());
+    self.selected(!self.selected());
   };
 };
 
@@ -36,10 +36,10 @@ var Collection = function (coll) {
   self.hovered = ko.observable(false);
 
   self.handleSelect = function (row, e) {
-    this.selected(!this.selected());
+    self.selected(!self.selected());
   };
   self.toggleHover = function (row, e) {
-    this.hovered(!this.hovered());
+    self.hovered(!self.hovered());
   };
 }
 
@@ -110,6 +110,11 @@ var SearchCollectionsModel = function (props) {
     $(document).trigger("confirmDelete");
   };
 
+  self.markManyForDeletion = function (collections) {
+    self.collectionToDelete = collections;
+    $(document).trigger("confirmDeleteMany")
+  };
+
   self.deleteCollection = function () {
     $(document).trigger("deleting");
     $.post(self.DELETE_URL,
@@ -122,6 +127,19 @@ var SearchCollectionsModel = function (props) {
       }, "json");
   };
 
+  self.deleteCollections = function () {
+    self.isLoading = true;
+    $(document).trigger("deleting");
+    $.post(self.DELETE_URL,
+    {
+      id: self.selectedCollections()
+    },
+    function (data) {
+      self.updateCollections();
+    }, "json");
+    $(document).trigger("collectionDeleted");
+  };
+
   self.copyCollection = function (collection) {
     $(document).trigger("copying");
     $.post(self.COPY_URL,
@@ -135,6 +153,19 @@ var SearchCollectionsModel = function (props) {
       }, "json");
   };
 
+  self.copyCollections = function (collections) {
+    $(document).trigger("copying");
+    $.post(self.COPY_URL,
+    {
+      id: self.selectedCollections(),
+      //type: coll.isCoreOnly() ? "core" : "collection"
+    },
+    function (data) {
+      self.updateCollections();
+    }, "json");
+    $(document).trigger("collectionCopied");
+  };
+
   self.updateCollections = function () {
     self.isLoading(true);
     $.getJSON(self.LIST_COLLECTIONS_URL, function (data) {
@@ -185,4 +216,18 @@ var SearchCollectionsModel = function (props) {
       }, "json");
   };
 
+  self.toggleSelectAll = function() { // duplicated from hue/desktop/libs/indexer/static/js/collections.js
+    var direction = !self.selectedCollections().length;
+    ko.utils.arrayForEach(self.filteredCollections(), function(collection) {
+      collection.selected(direction);
+    });
+  };
+
+  self.toggleCollectionSelect = function(collection, e) { // duplicated from hue/desktop/libs/indexer/static/js/collections.js
+    ko.utils.arrayForEach(self.collections(), function(other_collection) {
+      if(ko.unwrap(other_collection).id() == collection.id()) {
+        other_collection.selected(!other_collection.selected());
+      }
+    });
+  };
 };

+ 51 - 1
desktop/core/static/css/hue3.css

@@ -1068,6 +1068,12 @@ a#advanced-btn:hover {
   border-color: #999999;
 }
 
+.hueCheckboxText {
+  left: 3px;
+  position: relative;
+  top: -3px;
+}
+
 .modal form {
   margin: 0;
 }
@@ -1663,4 +1669,48 @@ border-bottom-left-radius: 0px;
 .app-icon {
   width: 24px;
   height: 24px;
-}
+}
+
+/*********************************************************
+ *
+ * Styles from admin_collections.mako template
+ *
+ ********************************************************/
+
+#collections {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+}
+
+.coll-heading {
+  position: relative;
+  top: 12px;
+}
+
+.placeholder {
+  background-color: #F5F5F5;
+  border: 1px solid #E3E3E3;
+  height: 40px;
+}
+
+.collection-actions {
+   cursor: pointer;
+   margin: 10px 10px 0 0;
+ }
+
+/*********************************************************
+ *
+ * Misc styles that can be reused as needed
+ * - from admin_collections.mako template
+ *
+ ********************************************************/
+ .spinner {
+   color: #ddd;
+   font-size: 60px;
+ }
+
+ .pointer {
+   cursor: pointer;
+ }