Added Server-side filtering for swift UI

Added server-side filtering for containers in swift UI using the
magic search directive.

Change-Id: Id0eaa818fab8b2c7d7a0ab40c1a943c2e7342f10
Partial-Implements: blueprint server-side-filtering
This commit is contained in:
Luis Daniel Castellanos 2016-06-08 11:19:37 -05:00
parent ce7dc70c7f
commit 277d15abff
13 changed files with 181 additions and 13 deletions

View File

@ -72,7 +72,8 @@
getName: getName, getName: getName,
getTextFacet: getTextFacet, getTextFacet: getTextFacet,
getUnusedFacetChoices: getUnusedFacetChoices, getUnusedFacetChoices: getUnusedFacetChoices,
getQueryPattern: getQueryPattern getQueryPattern: getQueryPattern,
getQueryObject: getQueryObject
}; };
return service; return service;
@ -324,6 +325,17 @@
} }
} }
} }
// Gets a query object from a query string
function getQueryObject(data) {
return getSearchTermsFromQueryString(data)
.map(getSearchTermObject)
.reduce(addToObject, {});
function addToObject(orig, curr) {
orig[curr.type] = curr.value;
return orig;
}
}
} }
})(); })();

View File

@ -325,5 +325,15 @@
{name: "other"}]); {name: "other"}]);
}); });
}); });
describe("getQueryObject", function() {
it("should get a query object from a query string", function() {
var queryString = "name=test&type=test";
var queryObject = service.getQueryObject(queryString);
expect(queryObject).toEqual({name: "test", type: "test"});
});
});
}); });
})(); })();

View File

@ -53,9 +53,18 @@ class Containers(generic.View):
def get(self, request): def get(self, request):
"""Get the list of containers for this account """Get the list of containers for this account
:param prefix: container name prefix value. Named items in the
response begin with this value
TODO(neillc): Add pagination TODO(neillc): Add pagination
""" """
containers, has_more = api.swift.swift_get_containers(request) prefix = request.GET.get('prefix', None)
if prefix:
containers, has_more = api.swift.\
swift_get_containers(request, prefix=prefix)
else:
containers, has_more = api.swift.swift_get_containers(request)
containers = [container.to_dict() for container in containers] containers = [container.to_dict() for container in containers]
return {'items': containers, 'has_more': has_more} return {'items': containers, 'has_more': has_more}

View File

@ -135,10 +135,11 @@ def swift_object_exists(request, container_name, object_name):
@profiler.trace @profiler.trace
def swift_get_containers(request, marker=None): def swift_get_containers(request, marker=None, prefix=None):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000) limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
headers, containers = swift_api(request).get_account(limit=limit + 1, headers, containers = swift_api(request).get_account(limit=limit + 1,
marker=marker, marker=marker,
prefix=prefix,
full_listing=True) full_listing=True)
container_objs = [Container(c) for c in containers] container_objs = [Container(c) for c in containers]
if(len(container_objs) > limit): if(len(container_objs) > limit):

View File

@ -63,6 +63,7 @@
updateContainer: updateContainer, updateContainer: updateContainer,
recursiveCollect: recursiveCollect, recursiveCollect: recursiveCollect,
recursiveDelete: recursiveDelete, recursiveDelete: recursiveDelete,
getContainers: getContainers,
_recursiveDeleteFiles: recursiveDeleteFiles, _recursiveDeleteFiles: recursiveDeleteFiles,
_recursiveDeleteFolders: recursiveDeleteFolders _recursiveDeleteFolders: recursiveDeleteFolders
@ -72,6 +73,8 @@
// initialisation completing (i.e. containers listing loaded) // initialisation completing (i.e. containers listing loaded)
model.intialiseDeferred = $q.defer(); model.intialiseDeferred = $q.defer();
model.getContainersDeferred = $q.defer();
return model; return model;
/** /**
@ -370,5 +373,25 @@
return deleteFolder(node.folder); return deleteFolder(node.folder);
}); });
} }
/**
* @ngdoc method
* @name ContainersModel.getContainers
*
* @param {Object} params Search parameters for filtering
* @description
* Gets the model containers filtered by the given query. If query is empty
* then it returns all of the containers
*
*/
function getContainers(params) {
swiftAPI.getContainers(params).then(function onContainers(data) {
model.containers.length = 0;
push.apply(model.containers, data.data.items);
}).then(function resolve() {
model.getContainersDeferred.resolve();
});
}
} }
})(); })();

View File

@ -192,6 +192,22 @@
expect(service.objects).toEqual([{name: 'two'}]); expect(service.objects).toEqual([{name: 'two'}]);
}); });
it('should fetch containers on demand', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getContainers').and.returnValue(deferred.promise);
var containers = ['two', 'items'];
var params = {};
service.getContainers(params);
expect(swiftAPI.getContainers).toHaveBeenCalledWith({});
deferred.resolve({data: {items: ['two', 'items']}});
$rootScope.$apply();
expect(containers).toEqual(['two', 'items']);
});
describe('recursive deletion', function describe() { describe('recursive deletion', function describe() {
// fake up a basic set of object listings to return from our fake getObjects // fake up a basic set of object listings to return from our fake getObjects
// below // below
@ -359,6 +375,27 @@
expect(state.deleted.folders).toEqual(4); expect(state.deleted.folders).toEqual(4);
expect(state.deleted.failures).toEqual(0); expect(state.deleted.failures).toEqual(0);
}); });
it('should increase state failures on deletion failure', function test() {
var state = {deleted: {folders: 0, failures: 0}};
var failures = [];
spyOn(apiService, 'delete').and.callFake(function fake(url) {
failures.push(url);
var deferred = $q.defer();
deferred.reject({status: 403});
return deferred.promise;
});
service._recursiveDeleteFolders(state, {tree: fakeTree});
$rootScope.$apply();
expect(failures).toEqual([
'/api/swift/containers/spam/object/folder/subfolder3/',
'/api/swift/containers/spam/object/folder/subfolder/',
'/api/swift/containers/spam/object/folder/subfolder2/',
'/api/swift/containers/spam/object/folder/'
]);
expect(state.deleted.failures).toEqual(4);
expect(state.deleted.folders).toEqual(0);
});
}); });
}); });
})(); })();

View File

@ -38,6 +38,9 @@
'horizon.framework.widgets.form.ModalFormService', 'horizon.framework.widgets.form.ModalFormService',
'horizon.framework.widgets.modal.simple-modal.service', 'horizon.framework.widgets.modal.simple-modal.service',
'horizon.framework.widgets.toast.service', 'horizon.framework.widgets.toast.service',
'horizon.framework.widgets.magic-search.events',
'horizon.framework.widgets.magic-search.service',
'$scope',
'$location', '$location',
'$q' '$q'
]; ];
@ -50,6 +53,9 @@
modalFormService, modalFormService,
simpleModalService, simpleModalService,
toastService, toastService,
magicSearchEvents,
magicSearchService,
$scope,
$location, $location,
$q) { $q) {
var ctrl = this; var ctrl = this;
@ -57,6 +63,20 @@
ctrl.model.initialize(); ctrl.model.initialize();
ctrl.baseRoute = baseRoute; ctrl.baseRoute = baseRoute;
ctrl.containerRoute = containerRoute; ctrl.containerRoute = containerRoute;
ctrl.filterFacets = [
{
name: 'prefix',
label: gettext('Prefix'),
singleton: true,
isServer: true
}];
// TODO(lcastell): remove this flag when the magic search bar can be used
// as an standalone component.
// This flag is used to tell what trigger the search updated event. Currently,
// when selecting a container the magic-search controller gets executed and emits
// the searchUpdated event.
ctrl.filterEventTrigeredBySearchBar = true;
ctrl.checkContainerNameConflict = checkContainerNameConflict; ctrl.checkContainerNameConflict = checkContainerNameConflict;
ctrl.toggleAccess = toggleAccess; ctrl.toggleAccess = toggleAccess;
@ -81,8 +101,12 @@
} }
function selectContainer(container) { function selectContainer(container) {
if (!ctrl.model.container || container.name !== ctrl.model.container.name) {
ctrl.filterEventTrigeredBySearchBar = false;
}
ctrl.model.container = container; ctrl.model.container = container;
$location.path(ctrl.containerRoute + container.name); $location.path(ctrl.containerRoute + container.name);
return ctrl.model.fetchContainerDetail(container); return ctrl.model.fetchContainerDetail(container);
} }
@ -225,5 +249,15 @@
} }
); );
} }
$scope.$on(magicSearchEvents.SEARCH_UPDATED, function(event, data) {
// At this moment there's only server side filtering supported, therefore
// there's no need to check if it is client side or server side filtering
if (ctrl.filterEventTrigeredBySearchBar) {
ctrl.model.getContainers(magicSearchService.getQueryObject(data));
}
else {
ctrl.filterEventTrigeredBySearchBar = true;
}
});
} }
})(); })();

View File

@ -25,6 +25,13 @@
var fakeModel = { var fakeModel = {
loadContainerContents: angular.noop, loadContainerContents: angular.noop,
initialize: angular.noop, initialize: angular.noop,
getContainers: function fake(query) {
return {
then: function then(callback) {
callback(query);
}
};
},
fetchContainerDetail: function fake() { fetchContainerDetail: function fake() {
return { return {
then: function then(callback) { then: function then(callback) {
@ -37,7 +44,8 @@
} }
}; };
var $q, $location, $rootScope, controller, modalFormService, simpleModal, swiftAPI, toast; var $q, scope, $location, $rootScope, controller,
modalFormService, simpleModal, swiftAPI, toast;
beforeEach(module('horizon.dashboard.project.containers', function($provide) { beforeEach(module('horizon.dashboard.project.containers', function($provide) {
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel); $provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
@ -48,11 +56,12 @@
$q = _$q_; $q = _$q_;
$location = $injector.get('$location'); $location = $injector.get('$location');
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
scope = $rootScope.$new();
modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService'); modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService');
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service'); simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift'); swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
toast = $injector.get('horizon.framework.widgets.toast.service'); toast = $injector.get('horizon.framework.widgets.toast.service');
fakeModel.getContainersDeferred = $q.defer();
spyOn(fakeModel, 'initialize'); spyOn(fakeModel, 'initialize');
spyOn(fakeModel, 'loadContainerContents'); spyOn(fakeModel, 'loadContainerContents');
spyOn(fakeModel, 'fetchContainerDetail').and.callThrough(); spyOn(fakeModel, 'fetchContainerDetail').and.callThrough();
@ -62,6 +71,7 @@
function createController() { function createController() {
return controller( return controller(
'horizon.dashboard.project.containers.ContainersController', { 'horizon.dashboard.project.containers.ContainersController', {
$scope:scope,
'horizon.dashboard.project.containers.baseRoute': 'base ham', 'horizon.dashboard.project.containers.baseRoute': 'base ham',
'horizon.dashboard.project.containers.containerRoute': 'eggs ' 'horizon.dashboard.project.containers.containerRoute': 'eggs '
}); });
@ -258,5 +268,28 @@
expect(fakeModel.containers[0].name).toEqual('spam'); expect(fakeModel.containers[0].name).toEqual('spam');
expect(fakeModel.containers.length).toEqual(1); expect(fakeModel.containers.length).toEqual(1);
}); });
it('should call getContainers when filters change', function test() {
spyOn(fakeModel, 'getContainers').and.callThrough();
var ctrl = createController();
ctrl.filterEventTrigeredBySearchBar = true;
scope.cc = ctrl;
scope.$digest();
scope.cc.currentSearchFacets = 'prefix=test';
scope.$digest();
scope.$emit('searchUpdated', scope.cc.currentSearchFacets);
expect(fakeModel.getContainers).toHaveBeenCalledWith({prefix: 'test'});
});
it('should not call getContainers when "searchUpdated" event was not ' +
'triggered by filters update', function test() {
spyOn(fakeModel, 'getContainers');
var ctrl = createController();
ctrl.filterEventTrigeredBySearchBar = false;
scope.cc = ctrl;
scope.$emit('searchUpdated', scope.cc.currentSearchFacets);
expect(fakeModel.getContainers).not.toHaveBeenCalled();
expect(ctrl.filterEventTrigeredBySearchBar).toEqual(true);
});
}); });
})(); })();

View File

@ -11,7 +11,10 @@
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<uib-accordion class="hz-container-accordion"> <hz-magic-search-context filter-facets="cc.filterFacets">
<hz-magic-search-bar></hz-magic-search-bar>
</hz-magic-search-context>
<uib-accordion class="hz-container-accordion" ng-if="cc.model.containers.length > 0">
<div uib-accordion-group ng-repeat="container in cc.model.containers" <div uib-accordion-group ng-repeat="container in cc.model.containers"
ng-class="{'panel-primary': container.name === cc.model.container.name}" ng-class="{'panel-primary': container.name === cc.model.container.name}"
class="panel-default" class="panel-default"
@ -42,15 +45,15 @@
<ul ng-if="container.is_fetched" class="hz-object-detail list-unstyled"> <ul ng-if="container.is_fetched" class="hz-object-detail list-unstyled">
<li class="hz-object-count row"> <li class="hz-object-count row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Object Count</span> <span class="hz-object-label col-lg-7 col-md-12" translate>Object Count</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.count$}</span> <span class="hz-object-val col-lg-5 col-md-12">{$ container.count $}</span>
</li> </li>
<li class="hz-object-size row"> <li class="hz-object-size row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Size</span> <span class="hz-object-label col-lg-7 col-md-12" translate>Size</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.bytes | bytes$}</span> <span class="hz-object-val col-lg-5 col-md-12">{$ container.bytes | bytes $}</span>
</li> </li>
<li class="hz-object-timestamp row"> <li class="hz-object-timestamp row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Date Created</span> <span class="hz-object-label col-lg-7 col-md-12" translate>Date Created</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.timestamp | date$}</span> <span class="hz-object-val col-lg-5 col-md-12">{$ container.timestamp | date $}</span>
</li> </li>
<li class="hz-object-link row"> <li class="hz-object-link row">
<div class="themable-checkbox col-lg-7 col-md-12"> <div class="themable-checkbox col-lg-7 col-md-12">
@ -68,6 +71,9 @@
</ul> </ul>
</div uib-accordion-group> </div uib-accordion-group>
</uib-accordion> </uib-accordion>
<div class="col-xs-12" ng-if="cc.model.containers.length == 0">
<p><translate>No items to display.</translate></p>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -97,8 +97,9 @@
* @returns {Object} An object with 'items' and 'has_more' flag. * @returns {Object} An object with 'items' and 'has_more' flag.
* *
*/ */
function getContainers() { function getContainers(params) {
return apiService.get('/api/swift/containers/') var config = params ? {'params': params} : {};
return apiService.get('/api/swift/containers/', config)
.error(function() { .error(function() {
toastService.add('error', gettext('Unable to get the Swift container listing.')); toastService.add('error', gettext('Unable to get the Swift container listing.'));
}); });

View File

@ -50,7 +50,8 @@
func: 'getContainers', func: 'getContainers',
method: 'get', method: 'get',
path: '/api/swift/containers/', path: '/api/swift/containers/',
error: 'Unable to get the Swift container listing.' error: 'Unable to get the Swift container listing.',
data: {}
}, },
{ {
func: 'getContainer', func: 'getContainer',

View File

@ -49,7 +49,7 @@ class SwiftRestTestCase(test.TestCase):
# #
@mock.patch.object(swift.api, 'swift') @mock.patch.object(swift.api, 'swift')
def test_containers_get(self, nc): def test_containers_get(self, nc):
request = self.mock_rest_request() request = self.mock_rest_request(GET={})
nc.swift_get_containers.return_value = (self._containers, False) nc.swift_get_containers.return_value = (self._containers, False)
response = swift.Containers().get(request) response = swift.Containers().get(request)
self.assertStatusCode(response, 200) self.assertStatusCode(response, 200)

View File

@ -33,6 +33,7 @@ class SwiftApiTests(test.APITestCase):
swift_api = self.stub_swiftclient() swift_api = self.stub_swiftclient()
swift_api.get_account(limit=1001, swift_api.get_account(limit=1001,
marker=None, marker=None,
prefix=None,
full_listing=True).AndReturn([{}, cont_data]) full_listing=True).AndReturn([{}, cont_data])
self.mox.ReplayAll() self.mox.ReplayAll()