From 277d15abfff90b5d78fc238cfe8829328fe160b4 Mon Sep 17 00:00:00 2001 From: Luis Daniel Castellanos Date: Wed, 8 Jun 2016 11:19:37 -0500 Subject: [PATCH] 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 --- .../magic-search/magic-search.service.js | 14 ++++++- .../magic-search/magic-search.service.spec.js | 10 +++++ openstack_dashboard/api/rest/swift.py | 11 +++++- openstack_dashboard/api/swift.py | 3 +- .../containers/containers-model.service.js | 23 ++++++++++++ .../containers-model.service.spec.js | 37 +++++++++++++++++++ .../containers/containers.controller.js | 34 +++++++++++++++++ .../containers/containers.controller.spec.js | 37 ++++++++++++++++++- .../project/containers/containers.html | 14 +++++-- .../openstack-service-api/swift.service.js | 5 ++- .../swift.service.spec.js | 3 +- .../test/api_tests/swift_rest_tests.py | 2 +- .../test/api_tests/swift_tests.py | 1 + 13 files changed, 181 insertions(+), 13 deletions(-) diff --git a/horizon/static/framework/widgets/magic-search/magic-search.service.js b/horizon/static/framework/widgets/magic-search/magic-search.service.js index 032856e539..91798826bb 100644 --- a/horizon/static/framework/widgets/magic-search/magic-search.service.js +++ b/horizon/static/framework/widgets/magic-search/magic-search.service.js @@ -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; + } + } } })(); diff --git a/horizon/static/framework/widgets/magic-search/magic-search.service.spec.js b/horizon/static/framework/widgets/magic-search/magic-search.service.spec.js index 3eeb79ad2a..cd6a4c6f16 100644 --- a/horizon/static/framework/widgets/magic-search/magic-search.service.spec.js +++ b/horizon/static/framework/widgets/magic-search/magic-search.service.spec.js @@ -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"}); + }); + + }); }); })(); diff --git a/openstack_dashboard/api/rest/swift.py b/openstack_dashboard/api/rest/swift.py index 5deeffa585..9419a1903d 100644 --- a/openstack_dashboard/api/rest/swift.py +++ b/openstack_dashboard/api/rest/swift.py @@ -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} diff --git a/openstack_dashboard/api/swift.py b/openstack_dashboard/api/swift.py index 1762a50a2b..fc34871a67 100644 --- a/openstack_dashboard/api/swift.py +++ b/openstack_dashboard/api/swift.py @@ -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): diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.js index e9fb9864ba..2ace67ed35 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.js @@ -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(); + }); + } + } })(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.spec.js index e3e5e565ad..b67cb5be65 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers-model.service.spec.js @@ -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); + }); }); }); })(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js index dc0f9a8fbc..2863800c10 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js @@ -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; + } + }); } })(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js index 1f282dcbf7..5dad58ab2c 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js @@ -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); + }); }); })(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html index 4dece5acee..704ee0e44f 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html @@ -11,7 +11,10 @@
- + + + +
  • Object Count - {$container.count$} + {$ container.count $}
  • Size - {$container.bytes | bytes$} + {$ container.bytes | bytes $}
  • Date Created - {$container.timestamp | date$} + {$ container.timestamp | date $}
  • diff --git a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js index eb4688ba36..221d3c606e 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js @@ -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.')); }); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js index 50a14abec5..2c05a9d806 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js @@ -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', diff --git a/openstack_dashboard/test/api_tests/swift_rest_tests.py b/openstack_dashboard/test/api_tests/swift_rest_tests.py index 839cebc843..4ea2963c48 100644 --- a/openstack_dashboard/test/api_tests/swift_rest_tests.py +++ b/openstack_dashboard/test/api_tests/swift_rest_tests.py @@ -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) diff --git a/openstack_dashboard/test/api_tests/swift_tests.py b/openstack_dashboard/test/api_tests/swift_tests.py index 36646a5418..e250124d4a 100644 --- a/openstack_dashboard/test/api_tests/swift_tests.py +++ b/openstack_dashboard/test/api_tests/swift_tests.py @@ -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()