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:
parent
ce7dc70c7f
commit
277d15abff
@ -72,7 +72,8 @@
|
||||
getName: getName,
|
||||
getTextFacet: getTextFacet,
|
||||
getUnusedFacetChoices: getUnusedFacetChoices,
|
||||
getQueryPattern: getQueryPattern
|
||||
getQueryPattern: getQueryPattern,
|
||||
getQueryObject: getQueryObject
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -325,5 +325,15 @@
|
||||
{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"});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -53,9 +53,18 @@ class Containers(generic.View):
|
||||
def get(self, request):
|
||||
"""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
|
||||
"""
|
||||
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]
|
||||
return {'items': containers, 'has_more': has_more}
|
||||
|
||||
|
@ -135,10 +135,11 @@ def swift_object_exists(request, container_name, object_name):
|
||||
|
||||
|
||||
@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)
|
||||
headers, containers = swift_api(request).get_account(limit=limit + 1,
|
||||
marker=marker,
|
||||
prefix=prefix,
|
||||
full_listing=True)
|
||||
container_objs = [Container(c) for c in containers]
|
||||
if(len(container_objs) > limit):
|
||||
|
@ -63,6 +63,7 @@
|
||||
updateContainer: updateContainer,
|
||||
recursiveCollect: recursiveCollect,
|
||||
recursiveDelete: recursiveDelete,
|
||||
getContainers: getContainers,
|
||||
|
||||
_recursiveDeleteFiles: recursiveDeleteFiles,
|
||||
_recursiveDeleteFolders: recursiveDeleteFolders
|
||||
@ -72,6 +73,8 @@
|
||||
// initialisation completing (i.e. containers listing loaded)
|
||||
model.intialiseDeferred = $q.defer();
|
||||
|
||||
model.getContainersDeferred = $q.defer();
|
||||
|
||||
return model;
|
||||
|
||||
/**
|
||||
@ -370,5 +373,25 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
||||
|
@ -192,6 +192,22 @@
|
||||
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() {
|
||||
// fake up a basic set of object listings to return from our fake getObjects
|
||||
// below
|
||||
@ -359,6 +375,27 @@
|
||||
expect(state.deleted.folders).toEqual(4);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -38,6 +38,9 @@
|
||||
'horizon.framework.widgets.form.ModalFormService',
|
||||
'horizon.framework.widgets.modal.simple-modal.service',
|
||||
'horizon.framework.widgets.toast.service',
|
||||
'horizon.framework.widgets.magic-search.events',
|
||||
'horizon.framework.widgets.magic-search.service',
|
||||
'$scope',
|
||||
'$location',
|
||||
'$q'
|
||||
];
|
||||
@ -50,6 +53,9 @@
|
||||
modalFormService,
|
||||
simpleModalService,
|
||||
toastService,
|
||||
magicSearchEvents,
|
||||
magicSearchService,
|
||||
$scope,
|
||||
$location,
|
||||
$q) {
|
||||
var ctrl = this;
|
||||
@ -57,6 +63,20 @@
|
||||
ctrl.model.initialize();
|
||||
ctrl.baseRoute = baseRoute;
|
||||
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.toggleAccess = toggleAccess;
|
||||
@ -81,8 +101,12 @@
|
||||
}
|
||||
|
||||
function selectContainer(container) {
|
||||
if (!ctrl.model.container || container.name !== ctrl.model.container.name) {
|
||||
ctrl.filterEventTrigeredBySearchBar = false;
|
||||
}
|
||||
ctrl.model.container = container;
|
||||
$location.path(ctrl.containerRoute + container.name);
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
@ -25,6 +25,13 @@
|
||||
var fakeModel = {
|
||||
loadContainerContents: angular.noop,
|
||||
initialize: angular.noop,
|
||||
getContainers: function fake(query) {
|
||||
return {
|
||||
then: function then(callback) {
|
||||
callback(query);
|
||||
}
|
||||
};
|
||||
},
|
||||
fetchContainerDetail: function fake() {
|
||||
return {
|
||||
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) {
|
||||
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
|
||||
@ -48,11 +56,12 @@
|
||||
$q = _$q_;
|
||||
$location = $injector.get('$location');
|
||||
$rootScope = _$rootScope_;
|
||||
scope = $rootScope.$new();
|
||||
modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService');
|
||||
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
|
||||
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||
|
||||
fakeModel.getContainersDeferred = $q.defer();
|
||||
spyOn(fakeModel, 'initialize');
|
||||
spyOn(fakeModel, 'loadContainerContents');
|
||||
spyOn(fakeModel, 'fetchContainerDetail').and.callThrough();
|
||||
@ -62,6 +71,7 @@
|
||||
function createController() {
|
||||
return controller(
|
||||
'horizon.dashboard.project.containers.ContainersController', {
|
||||
$scope:scope,
|
||||
'horizon.dashboard.project.containers.baseRoute': 'base ham',
|
||||
'horizon.dashboard.project.containers.containerRoute': 'eggs '
|
||||
});
|
||||
@ -258,5 +268,28 @@
|
||||
expect(fakeModel.containers[0].name).toEqual('spam');
|
||||
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);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -11,7 +11,10 @@
|
||||
|
||||
<div class="row">
|
||||
<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"
|
||||
ng-class="{'panel-primary': container.name === cc.model.container.name}"
|
||||
class="panel-default"
|
||||
@ -42,15 +45,15 @@
|
||||
<ul ng-if="container.is_fetched" class="hz-object-detail list-unstyled">
|
||||
<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-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 class="hz-object-size row">
|
||||
<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 class="hz-object-timestamp row">
|
||||
<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 class="hz-object-link row">
|
||||
<div class="themable-checkbox col-lg-7 col-md-12">
|
||||
@ -68,6 +71,9 @@
|
||||
</ul>
|
||||
</div uib-accordion-group>
|
||||
</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>
|
||||
|
@ -97,8 +97,9 @@
|
||||
* @returns {Object} An object with 'items' and 'has_more' flag.
|
||||
*
|
||||
*/
|
||||
function getContainers() {
|
||||
return apiService.get('/api/swift/containers/')
|
||||
function getContainers(params) {
|
||||
var config = params ? {'params': params} : {};
|
||||
return apiService.get('/api/swift/containers/', config)
|
||||
.error(function() {
|
||||
toastService.add('error', gettext('Unable to get the Swift container listing.'));
|
||||
});
|
||||
|
@ -50,7 +50,8 @@
|
||||
func: 'getContainers',
|
||||
method: 'get',
|
||||
path: '/api/swift/containers/',
|
||||
error: 'Unable to get the Swift container listing.'
|
||||
error: 'Unable to get the Swift container listing.',
|
||||
data: {}
|
||||
},
|
||||
{
|
||||
func: 'getContainer',
|
||||
|
@ -49,7 +49,7 @@ class SwiftRestTestCase(test.TestCase):
|
||||
#
|
||||
@mock.patch.object(swift.api, 'swift')
|
||||
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)
|
||||
response = swift.Containers().get(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
|
@ -33,6 +33,7 @@ class SwiftApiTests(test.APITestCase):
|
||||
swift_api = self.stub_swiftclient()
|
||||
swift_api.get_account(limit=1001,
|
||||
marker=None,
|
||||
prefix=None,
|
||||
full_listing=True).AndReturn([{}, cont_data])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user