Browse Source

[search] Unified import and list collection page

Enrico Berti 12 years ago
parent
commit
b668ced

+ 0 - 0
apps/search/src/search/search_controler.py → apps/search/src/search/search_controller.py


+ 1 - 1
apps/search/src/search/templates/admin_collection_facets.mako

@@ -22,7 +22,7 @@
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="macros" file="macros.mako" />
 
 
-${ commonheader(_('Search'), "search", user) | n,unicode }
+${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 
 
 <%layout:skeleton>
 <%layout:skeleton>
   <%def name="title()">
   <%def name="title()">

+ 1 - 1
apps/search/src/search/templates/admin_collection_highlighting.mako

@@ -22,7 +22,7 @@
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="macros" file="macros.mako" />
 
 
-${ commonheader(_('Search'), "search", user) | n,unicode }
+${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 
 
 <%layout:skeleton>
 <%layout:skeleton>
   <%def name="title()">
   <%def name="title()">

+ 1 - 1
apps/search/src/search/templates/admin_collection_properties.mako

@@ -24,7 +24,7 @@
 <%namespace name="utils" file="utils.inc.mako" />
 <%namespace name="utils" file="utils.inc.mako" />
 
 
 
 
-${ commonheader(_('Search'), "search", user) | n,unicode }
+${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 
 
 <%def name="indexProperty(key)">
 <%def name="indexProperty(key)">
   %if key in solr_collection["status"][hue_collection.name]["index"]:
   %if key in solr_collection["status"][hue_collection.name]["index"]:

+ 1 - 1
apps/search/src/search/templates/admin_collection_sorting.mako

@@ -22,7 +22,7 @@
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="macros" file="macros.mako" />
 
 
-${ commonheader(_('Search'), "search", user) | n,unicode }
+${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 
 
 <%layout:skeleton>
 <%layout:skeleton>
   <%def name="title()">
   <%def name="title()">

+ 1 - 1
apps/search/src/search/templates/admin_collection_template.mako

@@ -22,7 +22,7 @@
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="macros" file="macros.mako" />
 
 
-${ commonheader(_('Search'), "search", user) | n,unicode }
+${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 
 
 <style type="text/css">
 <style type="text/css">
   .CodeMirror {
   .CodeMirror {

+ 97 - 8
apps/search/src/search/templates/admin_collections.mako

@@ -22,10 +22,18 @@
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="actionbar" file="actionbar.mako" />
 
 
-${ commonheader(_('Search'), "search", user) | n,unicode }
+${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 
 
 <link rel="stylesheet" href="/search/static/css/admin.css">
 <link rel="stylesheet" href="/search/static/css/admin.css">
 
 
+<div class="search-bar" style="height: 30px">
+    <div class="pull-right" style="margin-top: 4px; margin-right: 20px">
+      <a href="${ url('search:index') }"><i class="icon-share-alt"></i> ${ _('Query UI') }</a>
+    </div>
+  &nbsp;
+</div>
+
+
 <div class="container-fluid">
 <div class="container-fluid">
   <h1>${_('Indexes')}</h1>
   <h1>${_('Indexes')}</h1>
 
 
@@ -33,22 +41,66 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
     <%def name="search()">
     <%def name="search()">
       <input type="text" placeholder="${_('Filter collections by name...')}" class="input-xxlarge search-query" id="filterInput">
       <input type="text" placeholder="${_('Filter collections by name...')}" class="input-xxlarge search-query" id="filterInput">
     </%def>
     </%def>
+
+    <%def name="creation()">
+      <button id="importBtn" type="button" class="btn"><i class="icon-plus-sign"></i> ${ _('Import') }</button>
+    </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 
   <div class="row-fluid">
   <div class="row-fluid">
     <div class="span12">
     <div class="span12">
-      <ul id="collections">
-      % for collection in hue_collections:
-        <li style="cursor: move" data-collection="${ collection.name }">
-          <a href="${ collection.get_absolute_url() }" class="pull-right" style="margin-top: 10px;margin-right: 10px"><i class="icon-edit"></i> ${_('Edit')}</a>
-          <h4><i class="icon-list"></i> ${ collection.name } - ${ collection.is_core_only }</h4>
-        </li>
-      % endfor
+      <ul id="collections" data-bind="template: {name: 'collectionTemplate', foreach: collections}">
+##      % for collection in existing_hue_collections:
+##        <li style="cursor: move" data-collection="${ collection.name }">
+##          <a href="${ collection.get_absolute_url() }" class="pull-right" style="margin-top: 10px;margin-right: 10px"><i class="icon-edit"></i> ${_('Edit')}</a>
+##          <h4><i class="icon-list"></i> ${ collection.name } - ${ collection.is_core_only } -- ${ collection.label } == ${ collection.id }</h4>
+##        </li>
+##      % endfor
       </ul>
       </ul>
     </div>
     </div>
   </div>
   </div>
+
+  <script id="collectionTemplate" type="text/html">
+    <li style="cursor: move">
+      <a data-bind="attr: {'href': absoluteUrl}" class="pull-right" style="margin-top: 10px;margin-right: 10px"><i class="icon-edit"></i> ${_('Edit')}</a>
+      <h4><i class="icon-list"></i> <span data-bind="text: label"></span></h4>
+    </li>
+  </script>
+
+</div>
+
+<div id="importDialog" class="modal hide fade">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal">&times;</button>
+    <h3>${ _('Import Collections and Cores') }</h3>
+  </div>
+  <div class="modal-body">
+    <img src="/static/art/spinner.gif" data-bind="visible: isLoadingImportables()" />
+    <div data-bind="visible: !isLoadingImportables()">
+      <h5>${ _('Collections') }</h5>
+      <div class="alert" data-bind="visible: importableCollections().length == 0">${ _('All available collections from the Solr URL in hue.ini have been imported.') }</div>
+      <table data-bind="visible: importableCollections().length > 0, template: {name: 'importableTemplate', foreach: importableCollections}"></table>
+
+      <h5 style="margin-top: 20px">${ _('Cores') }</h5>
+      <div class="alert" data-bind="visible: importableCores().length == 0">${ _('All available cores from the Solr URL in hue.ini have been imported.') }</div>
+      <table data-bind="visible: importableCores().length > 0, template: {name: 'importableTemplate', foreach: importableCores}"></table>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <a href="javascript:void(0)" class="btn" data-dismiss="modal">${ _('Cancel') }</a>
+    <button href="javascript:void(0)" class="btn btn-primary" data-bind="enable: selectedImportableCollections().length > 0 || selectedImportableCores().length > 0, click: importCollectionsAndCores">${ _('Import Selected') }</button>
+  </div>
 </div>
 </div>
 
 
+<script id="importableTemplate" type="text/html">
+  <tr>
+    <td width="24">
+      <div data-bind="click: handleSelect, css: {hueCheckbox: true, 'icon-ok': selected}"></div>
+    </td>
+    <td data-bind="text: name"></td>
+  </tr>
+</script>
+
 <style type="text/css">
 <style type="text/css">
   #collections {
   #collections {
     list-style-type: none;
     list-style-type: none;
@@ -71,10 +123,25 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
   }
   }
 </style>
 </style>
 
 
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+<script src="/search/static/js/search.ko.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/jquery/plugins/jquery-ui-draggable-droppable-sortable-1.8.23.min.js"></script>
 <script src="/static/ext/js/jquery/plugins/jquery-ui-draggable-droppable-sortable-1.8.23.min.js"></script>
 
 
 <script type="text/javascript">
 <script type="text/javascript">
+
+  var appProperties = {
+    labels: [],
+    listCollectionsUrl: "${ url("search:admin_collections") }?format=json",
+    listImportablesUrl: "${ url("search:admin_collections_import") }?format=json",
+    importUrl: "${ url("search:admin_collections_import") }"
+  }
+
+  var viewModel = new SearchCollectionsModel(appProperties);
+  ko.applyBindings(viewModel);
+
   $(document).ready(function () {
   $(document).ready(function () {
+    viewModel.updateCollections();
+
     var orderedCores;
     var orderedCores;
     serializeList();
     serializeList();
     $("#collections").sortable({
     $("#collections").sortable({
@@ -107,6 +174,28 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         });
         });
       }, 300);
       }, 300);
     });
     });
+
+    $("#importDialog").modal({
+      show: false
+    });
+
+    % if is_redirect:
+        showImportDialog();
+    % endif
+
+    $("#importBtn").on("click", function () {
+      showImportDialog();
+    });
+
+    function showImportDialog() {
+      $("#importDialog").modal('show');
+      viewModel.updateImportables();
+    }
+
+    $(document).on("imported", function () {
+      $("#importDialog").modal('hide');
+    });
+
   });
   });
 </script>
 </script>
 
 

+ 0 - 110
apps/search/src/search/templates/admin_collections_wizard.mako

@@ -1,110 +0,0 @@
-## 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.
-
-<%!
-  from desktop.views import commonheader, commonfooter
-  from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="macros" file="macros.mako" />
-<%namespace name="actionbar" file="actionbar.mako" />
-
-${ commonheader(_('Search'), "search", user) | n,unicode }
-
-<link rel="stylesheet" href="/search/static/css/admin.css">
-
-<div class="container-fluid">
-  <h1>${_('Import a new collection')}</h1>
-
-  <div class="row-fluid">
-    <div class="span12">
-      <ul id="collections">
-      % for collection in collections:
-        <li>
-          <a class="addCollection" data-name="${ collection }" data-type="collection">
-            <h4><i class="icon-list"></i> ${ collection }</h4>
-          </a>
-        </li>
-      % endfor
-      % if not collections:
-        ## TODO: maybe better to list them as disabled with imported icon etc
-        ${ _('All available collections from the Solr URL in hue.ini have been imported.') }      
-      % endif
-      </ul>
-    </div>
-  </div>
-
-  <h1>${_('Import a new core')}</h1>
-
-  <div class="row-fluid">
-    <div class="span12">
-      <ul id="collections">
-      % for core in cores:
-        <li>
-          <a class="addCollection" data-name="${ core }" data-type="core">
-            <h4><i class="icon-list"></i> ${ core }</h4>
-          </a>
-        </li>
-      % endfor
-      % if not cores:
-        ${ _('All available cores Solr URL in hue.ini have been imported.') }      
-      % endif      
-      </ul>
-    </div>
-  </div>
-
-</div>
-
-<style type="text/css">
-  #collections {
-    list-style-type: none;
-    margin: 0;
-    padding: 0;
-    width: 100%;
-  }
-
-  #collections li {
-    margin-bottom: 10px;
-    padding: 10px;
-    border: 1px solid #E3E3E3;
-    height: 40px;
-  }
-
-  .placeholder {
-    height: 40px;
-    background-color: #F5F5F5;
-    border: 1px solid #E3E3E3;
-  }
-</style>
-
-<script type="text/javascript">
-  $(document).ready(function () {
-    $(".addCollection").click(function() {
-      var collectionName = $(this).data('name');
-      var collectionType = $(this).data('type');
-      $.post('${ url("search:admin_collections_wizard") }', {type: collectionType, name: collectionName},
-        function(response) {
-          if (response['status'] != 0) {
-            $.jHueNotify.error("${ _('Problem: ') }" + response['message']);
-          } else {
-            window.location = "/search/admin/collection/" + collectionName;
-          }
-	  });
-    });
-  });
-</script>
-
-${ commonfooter(messages) | n,unicode }

+ 9 - 0
apps/search/src/search/templates/layout.mako

@@ -26,6 +26,15 @@
   <script src="/static/ext/chosen/chosen.jquery.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="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
   <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 
 
+
+  <div class="search-bar" style="height: 30px">
+      <div class="pull-right" style="margin-top: 4px; margin-right: 20px">
+        <a href="${ url('search:index') }"><i class="icon-share-alt"></i> ${ _('Query UI') }</a> &nbsp; &nbsp;
+        <a href="${ url('search:admin_collections') }"><i class="icon-edit"></i> ${ _('Collection manager') }</a>
+      </div>
+    &nbsp;
+  </div>
+
   <div class="container-fluid">
   <div class="container-fluid">
     %if hasattr(caller, "title"):
     %if hasattr(caller, "title"):
       ${caller.title()}
       ${caller.title()}

+ 1 - 2
apps/search/src/search/templates/search.mako

@@ -31,9 +31,8 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 <div class="search-bar">
 <div class="search-bar">
   % if user.is_superuser:
   % if user.is_superuser:
     <div class="pull-right" style="margin-top: 4px">
     <div class="pull-right" style="margin-top: 4px">
-      <a class="change-settings" href="#"><i class="icon-edit"></i> ${ _('Customize collection display') }</a>
+      <a class="change-settings" href="#"><i class="icon-edit"></i> ${ _('Customize this collection') }</a> &nbsp;&nbsp;
       <a href="${ url('search:admin_collections') }"><i class="icon-edit"></i> ${ _('Collection manager') }</a>
       <a href="${ url('search:admin_collections') }"><i class="icon-edit"></i> ${ _('Collection manager') }</a>
-      <a href="${ url('search:admin_collections_wizard') }"><i class="icon-edit"></i> ${ _('Add collection') }</a>
     </div>
     </div>
   % endif
   % endif
   <form class="form-search" style="margin: 0">
   <form class="form-search" style="margin: 0">

+ 2 - 2
apps/search/src/search/urls.py

@@ -21,8 +21,8 @@ urlpatterns = patterns('search.views',
   url(r'^$', 'index', name='index'),
   url(r'^$', 'index', name='index'),
   url(r'^query$', 'index', name='query'),
   url(r'^query$', 'index', name='query'),
 
 
-  url(r'^admin/collections_$', 'admin_collections', name='admin_collections'),
-  url(r'^admin/collections_wizard$', 'admin_collections_wizard', name='admin_collections_wizard'),
+  url(r'^admin/collections$', 'admin_collections', name='admin_collections'),
+  url(r'^admin/collections_import$', 'admin_collections_import', name='admin_collections_import'),
 
 
   url(r'^admin/collection/(?P<collection>\w+)$', 'admin_collection_template', name='admin_collection'),
   url(r'^admin/collection/(?P<collection>\w+)$', 'admin_collection_template', name='admin_collection'),
   url(r'^admin/collection/(?P<collection>\w+)/properties$', 'admin_collection_properties', name='admin_collection_properties'),
   url(r'^admin/collection/(?P<collection>\w+)/properties$', 'admin_collection_properties', name='admin_collection_properties'),

+ 58 - 21
apps/search/src/search/views.py

@@ -35,7 +35,7 @@ from search.conf import SOLR_URL
 from search.decorators import allow_admin_only
 from search.decorators import allow_admin_only
 from search.forms import QueryForm, CollectionForm, HighlightingForm
 from search.forms import QueryForm, CollectionForm, HighlightingForm
 from search.models import Collection, augment_solr_response
 from search.models import Collection, augment_solr_response
-from search.search_controler import SearchController
+from search.search_controller import SearchController
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
@@ -45,7 +45,7 @@ def index(request):
 
 
   if not hue_collections:
   if not hue_collections:
     if request.user.is_superuser:
     if request.user.is_superuser:
-      return admin_collections_wizard(request)
+      return admin_collections(request, True)
     else:
     else:
       return no_collections(request)
       return no_collections(request)
 
 
@@ -99,34 +99,71 @@ def no_collections(request):
 
 
 
 
 @allow_admin_only
 @allow_admin_only
-def admin_collections(request):
-  hue_collections = Collection.objects.all()
+def admin_collections(request, is_redirect=False):
+  existing_hue_collections = Collection.objects.all()
+
+  if request.GET.get('format') == 'json':
+    collections = []
+    for collection in existing_hue_collections:
+      massaged_collection = {
+        'id': collection.id,
+        'name': collection.name,
+        'label': collection.label,
+        'isCoreOnly': collection.is_core_only,
+        'absoluteUrl': collection.get_absolute_url()
+      }
+      collections.append(massaged_collection)
+    return HttpResponse(json.dumps(collections), mimetype="application/json")
 
 
   return render('admin_collections.mako', request, {
   return render('admin_collections.mako', request, {
-    'hue_collections': hue_collections,
+    'existing_hue_collections': existing_hue_collections,
+    'is_redirect': is_redirect
   })
   })
 
 
 
 
 @allow_admin_only
 @allow_admin_only
-def admin_collections_wizard(request):
-  searcher = SearchController()
-
+def admin_collections_import(request):
   if request.method == 'POST':
   if request.method == 'POST':
-    result = {'status': -1, 'message': 'Error'}
-    try:
-      searcher.add_new_collection(request.POST.copy())
-      result['status'] = 0
-      request.info(_('Collection added!'))
-    except Exception, e:
-      result['message'] = unicode(str(e), "utf8")
+    searcher = SearchController()
+    status = 0
+    err_message = _('Error')
+    result = {
+      'status': status,
+      'message': err_message
+    }
+    importables = json.loads(request.POST["selected"])
+    for imp in importables:
+      try:
+        searcher.add_new_collection(imp)
+        status += 1
+      except Exception, e:
+        err_message += unicode(str(e), "utf8") + "\n"
+      result['message'] = status == len(importables) and _('Imported successfully') or _('Imported with errors: ') + err_message
     return HttpResponse(json.dumps(result), mimetype="application/json")
     return HttpResponse(json.dumps(result), mimetype="application/json")
   else:
   else:
-    collections = searcher.get_new_collections()
-    cores = searcher.get_new_cores()
-    return render('admin_collections_wizard.mako', request, {
-      'collections': collections,
-      'cores': cores,
-    })
+    if request.GET.get('format') == 'json':
+      searcher = SearchController()
+      new_solr_collections = searcher.get_new_collections()
+      massaged_collections = []
+      for coll in new_solr_collections:
+        massaged_collections.append({
+          'type': 'collection',
+          'name': coll
+        })
+      new_solr_cores = searcher.get_new_cores()
+      massaged_cores = []
+      for core in new_solr_cores:
+        massaged_cores.append({
+          'type': 'core',
+          'name': core
+        })
+      response = {
+        'newSolrCollections': list(massaged_collections),
+        'newSolrCores': list(massaged_cores)
+      }
+      return HttpResponse(json.dumps(response), mimetype="application/json")
+    else:
+      return admin_collections(request, True)
 
 
 
 
 @allow_admin_only
 @allow_admin_only

+ 15 - 1
apps/search/static/css/admin.css

@@ -58,4 +58,18 @@ h3 {
 
 
 .select-mini {
 .select-mini {
   width: 100%;
   width: 100%;
-}
+}
+
+.search-bar {
+  background-color: #fafafa;
+  border-bottom: 1px solid #e3e3e3;
+  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+  margin: 0;
+  margin-bottom: 20px;
+  padding: 10px;
+  padding-left: 20px;
+  padding-right: 20px;
+  text-align: left;
+}

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

@@ -60,6 +60,11 @@
 
 
 .facet-list li {
 .facet-list li {
   padding-left: 8px;
   padding-left: 8px;
+  padding-right: 3px;
+}
+
+.facet-list li a {
+  word-wrap: break-word;
 }
 }
 
 
 .counter {
 .counter {

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

@@ -0,0 +1,154 @@
+// 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.
+
+var Importable = function (importable) {
+  var self = this;
+  self.type = ko.observable(importable.type);
+  self.name = ko.observable(importable.name);
+  self.selected = ko.observable(false);
+  self.handleSelect = function (row, e) {
+    this.selected(!this.selected());
+  };
+};
+
+var Collection = function (coll) {
+  var self = this;
+
+  self.id = ko.observable(coll.id);
+  self.name = ko.observable(coll.name);
+  self.label = ko.observable(coll.label);
+  self.isCoreOnly = ko.observable(coll.isCoreOnly);
+  self.absoluteUrl = ko.observable(coll.absoluteUrl);
+  self.selected = ko.observable(false);
+  self.hovered = ko.observable(false);
+
+  self.handleSelect = function (row, e) {
+    this.selected(!this.selected());
+  };
+  self.toggleHover = function (row, e) {
+    this.hovered(!this.hovered());
+  };
+}
+
+var SearchCollectionsModel = function (props) {
+  var self = this;
+
+  self.LABELS = props.labels;
+
+  self.LIST_COLLECTIONS_URL = props.listCollectionsUrl;
+  self.LIST_IMPORTABLES_URL = props.listImportablesUrl;
+  self.IMPORT_URL = props.importUrl;
+
+  self.isLoading = ko.observable(false);
+  self.isLoadingImportables = ko.observable(false);
+  self.allSelected = ko.observable(false);
+
+  self.collections = ko.observableArray([]);
+  self.filteredCollections = ko.observableArray(self.collections());
+
+  self.importableCollections = ko.observableArray([]);
+  self.importableCores = ko.observableArray([]);
+
+  self.selectedCollections = ko.computed(function () {
+    return ko.utils.arrayFilter(self.collections(), function (coll) {
+      return coll.selected();
+    });
+  }, self);
+
+  self.selectedImportableCollections = ko.computed(function () {
+    return ko.utils.arrayFilter(self.importableCollections(), function (imp) {
+      return imp.selected();
+    });
+  }, self);
+
+  self.selectedImportableCores = ko.computed(function () {
+    return ko.utils.arrayFilter(self.importableCores(), function (imp) {
+      return imp.selected();
+    });
+  }, self);
+
+  self.selectAll = function () {
+    self.allSelected(!self.allSelected());
+    ko.utils.arrayForEach(self.collections(), function (coll) {
+      coll.selected(self.allSelected());
+    });
+    return true;
+  };
+
+  self.filterCollections = function (filter) {
+    self.filteredCollections(ko.utils.arrayFilter(self.collections(), function (coll) {
+      return coll.name().toLowerCase().indexOf(filter.toLowerCase()) > -1
+    }));
+  };
+
+  self.deleteCollections = function () {
+    var ids = [];
+    $(self.selectedCollections()).each(function (index, coll) {
+      ids.push(coll.id());
+    });
+    //callDelete(ids);
+  };
+
+  self.updateCollections = function () {
+    $.getJSON(self.LIST_COLLECTIONS_URL, function (data) {
+      self.collections(ko.utils.arrayMap(data, function (coll) {
+        return new Collection(coll);
+      }));
+      self.filteredCollections(self.collections());
+      $(document).trigger("collectionsRefreshed");
+    });
+  };
+
+  self.updateImportables = function () {
+    self.isLoadingImportables(true);
+    $.getJSON(self.LIST_IMPORTABLES_URL, function (data) {
+      self.importableCollections(ko.utils.arrayMap(data.newSolrCollections, function (coll) {
+        return new Importable(coll);
+      }));
+      self.importableCores(ko.utils.arrayMap(data.newSolrCores, function (core) {
+        return new Importable(core);
+      }));
+      self.isLoadingImportables(false);
+    });
+  };
+
+
+  self.importCollectionsAndCores = function () {
+    $(document).trigger("importing");
+    var selected = [];
+    ko.utils.arrayForEach(self.selectedImportableCollections(), function (imp) {
+      selected.push({
+        type: imp.type(),
+        name: imp.name()
+      });
+    });
+    ko.utils.arrayForEach(self.selectedImportableCores(), function (imp) {
+      selected.push({
+        type: imp.type(),
+        name: imp.name()
+      });
+    });
+    $.post(self.IMPORT_URL,
+      {
+        selected: ko.toJSON(selected)
+      },
+      function (data) {
+        $(document).trigger("imported");
+        self.updateCollections();
+      }, "json");
+  };
+
+};