diff --git a/doc/source/topics/angularjs.rst b/doc/source/topics/angularjs.rst index cadc8e02c1..7dc8db0ce0 100644 --- a/doc/source/topics/angularjs.rst +++ b/doc/source/topics/angularjs.rst @@ -314,3 +314,39 @@ defined in your enabled file, and add the relevant filepath, as below: read more in the `SASS documentation`_. .. _SASS documentation: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#import + +Schema Forms +============ + +`JSON schemas`_ are used to define model layout and then `angular-schema-form`_ is +used to create forms from that schema. Horizon adds some functionality on top of +that to make things even easier through ``ModalFormService`` which will open a +modal with the form inside. + +A very simple example:: + + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: "Name", description: "Name or alias" }, + title: { + type: "string", + enum: ['dr','jr','sir','mrs','mr','NaN','dj'] + } + } + }; + var model = {name: '', title: ''}; + var config = { + title: gettext('Create Container'), + schema: schema, + form: ['*'], + model: model + }; + ModalFormService.open(config).then(submit); // returns a promise + + function submit() { + // do something with model.name and model.title + } + +.. _JSON schemas: http://json-schema.org/ +.. _angular-schema-form: https://github.com/json-schema-form/angular-schema-form/blob/master/docs/index.md 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 2055b76d11..dc0f9a8fbc 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 @@ -35,10 +35,11 @@ 'horizon.dashboard.project.containers.basePath', 'horizon.dashboard.project.containers.baseRoute', 'horizon.dashboard.project.containers.containerRoute', + 'horizon.framework.widgets.form.ModalFormService', 'horizon.framework.widgets.modal.simple-modal.service', 'horizon.framework.widgets.toast.service', '$location', - '$modal' + '$q' ]; function ContainersController(swiftAPI, @@ -46,16 +47,18 @@ basePath, baseRoute, containerRoute, + modalFormService, simpleModalService, toastService, $location, - $modal) { + $q) { var ctrl = this; ctrl.model = containersModel; ctrl.model.initialize(); ctrl.baseRoute = baseRoute; ctrl.containerRoute = containerRoute; + ctrl.checkContainerNameConflict = checkContainerNameConflict; ctrl.toggleAccess = toggleAccess; ctrl.deleteContainer = deleteContainer; ctrl.deleteContainerAction = deleteContainerAction; @@ -64,6 +67,18 @@ ctrl.selectContainer = selectContainer; ////////// + function checkContainerNameConflict(containerName) { + if (!containerName) { + // consider empty model valid + return $q.when(); + } + + var def = $q.defer(); + // reverse the sense here - successful lookup == error so we reject the + // name if we find it in swift + swiftAPI.getContainer(containerName, true).then(def.reject, def.resolve); + return def.promise; + } function selectContainer(container) { ctrl.model.container = container; @@ -97,7 +112,7 @@ title: gettext('Confirm Delete'), body: interpolate( gettext('Are you sure you want to delete container %(name)s?'), container, true - ), + ), submit: gettext('Yes'), cancel: gettext('No') }; @@ -129,25 +144,84 @@ }); } + var createContainerSchema = { + type: 'object', + properties: { + name: { + title: gettext('Container Name'), + type: 'string', + pattern: '^[^/]+$', + description: gettext('Container name must not contain "/".') + }, + public: { + title: gettext('Container Access'), + type: 'boolean', + default: false, + description: gettext('A Public Container will allow anyone with the Public URL to ' + + 'gain access to your objects in the container.') + } + }, + required: ['name'] + }; + + var createContainerForm = [ + { + type: 'section', + htmlClass: 'row', + items: [ + { + type: 'section', + htmlClass: 'col-sm-6', + items: [ + { + key: 'name', + validationMessage: { + exists: gettext('A container with that name exists.') + }, + $asyncValidators: { + exists: checkContainerNameConflict + } + }, + { + key: 'public', + type: 'radiobuttons', + disableSuccessState: true, + titleMap: [ + { value: true, name: gettext('Public') }, + { value: false, name: gettext('Not public') } + ] + } + ] + }, + { + type: 'template', + templateUrl: basePath + 'create-container.help.html' + } + ] + } + ]; + function createContainer() { - var localSpec = { - backdrop: 'static', - controller: 'CreateContainerModalController as ctrl', - templateUrl: basePath + 'create-container-modal.html' + var model = {name: '', public: false}; + var config = { + title: gettext('Create Container'), + schema: createContainerSchema, + form: createContainerForm, + model: model }; - $modal.open(localSpec).result.then(function create(result) { - return ctrl.createContainerAction(result); + return modalFormService.open(config).then(function then() { + return ctrl.createContainerAction(model); }); } - function createContainerAction(result) { - swiftAPI.createContainer(result.name, result.public).then( + function createContainerAction(model) { + return swiftAPI.createContainer(model.name, model.public).then( function success() { toastService.add('success', interpolate( - gettext('Container %(name)s created.'), result, true + gettext('Container %(name)s created.'), model, true )); // generate a table row with no contents - ctrl.model.containers.push({name: result.name, count: 0, bytes: 0}); + ctrl.model.containers.push({name: model.name, count: 0, bytes: 0}); } ); } 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 27251fbd34..1f282dcbf7 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 @@ -37,18 +37,18 @@ } }; - var $q, $modal, $location, $rootScope, controller, simpleModal, swiftAPI, toast; + var $q, $location, $rootScope, controller, modalFormService, simpleModal, swiftAPI, toast; beforeEach(module('horizon.dashboard.project.containers', function($provide) { $provide.value('horizon.dashboard.project.containers.containers-model', fakeModel); })); - beforeEach(inject(function ($injector, _$q_, _$modal_, _$rootScope_) { + beforeEach(inject(function ($injector, _$q_, _$rootScope_) { controller = $injector.get('$controller'); $q = _$q_; $location = $injector.get('$location'); - $modal = _$modal_; $rootScope = _$rootScope_; + 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'); @@ -180,24 +180,69 @@ it('should open a dialog for creation', function test() { var deferred = $q.defer(); - var result = { result: deferred.promise }; - spyOn($modal, 'open').and.returnValue(result); + spyOn(modalFormService, 'open').and.returnValue(deferred.promise); var ctrl = createController(); spyOn(ctrl, 'createContainerAction'); ctrl.createContainer(); - expect($modal.open).toHaveBeenCalled(); - var spec = $modal.open.calls.mostRecent().args[0]; - expect(spec.backdrop).toBeDefined(); - expect(spec.controller).toEqual('CreateContainerModalController as ctrl'); - expect(spec.templateUrl).toBeDefined(); + expect(modalFormService.open).toHaveBeenCalled(); + var config = modalFormService.open.calls.mostRecent().args[0]; + expect(config.model).toBeDefined(); + expect(config.schema).toBeDefined(); + expect(config.form).toBeDefined(); - // when the modal is resolved, make sure delete is called - deferred.resolve('spam'); + // when the modal is resolved, make sure create is called + deferred.resolve(); $rootScope.$apply(); - expect(ctrl.createContainerAction).toHaveBeenCalledWith('spam'); + expect(ctrl.createContainerAction).toHaveBeenCalledWith({name: '', public: false}); + }); + + it('should check for container existence - with presence', function test() { + var deferred = $q.defer(); + spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise); + + var ctrl = createController(); + var d = ctrl.checkContainerNameConflict('spam'); + var resolved, rejected; + + // pretend getContainer found something + d.then(function result() { resolved = true; }, function () { rejected = true; }); + + expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true); + + // we found something + deferred.resolve(); + $rootScope.$apply(); + expect(rejected).toEqual(true); + expect(resolved).toBeUndefined(); + }); + + it('should check for container existence - with absence', function test() { + var deferred = $q.defer(); + spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise); + + var ctrl = createController(); + var d = ctrl.checkContainerNameConflict('spam'); + var resolved, rejected; + + d.then(function result() { resolved = true; }, function () { rejected = true; }); + + expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true); + + // we did not find something + deferred.reject(); + $rootScope.$apply(); + expect(resolved).toEqual(true); + expect(rejected).toBeUndefined(); + }); + + it('should not check for container existence sometimes', function test() { + spyOn(swiftAPI, 'getContainer'); + var ctrl = createController(); + ctrl.checkContainerNameConflict(''); + expect(swiftAPI.getContainer).not.toHaveBeenCalled(); }); it('should create containers', function test() { diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.controller.js deleted file mode 100644 index 87c5f4a3aa..0000000000 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.controller.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * (c) Copyright 2016 Rackspace US, Inc - * - * Licensed 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. - */ -(function () { - 'use strict'; - - angular - .module('horizon.dashboard.project.containers') - .controller('CreateContainerModalController', CreateContainerModalController); - - function CreateContainerModalController() { - var ctrl = this; - ctrl.model = { name: '', public: false}; - } -})(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.controller.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.controller.spec.js deleted file mode 100644 index 8c16af205e..0000000000 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.controller.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * (c) Copyright 2016 Rackspace US, Inc - * - * Licensed 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. - */ - -(function() { - 'use strict'; - - describe('horizon.dashboard.project.containers create-container controller', function() { - var controller; - - beforeEach(module('horizon.dashboard.project')); - beforeEach(module('horizon.dashboard.project.containers')); - - beforeEach(inject(function ($injector) { - controller = $injector.get('$controller'); - })); - - function createController() { - return controller('CreateContainerModalController', {}); - } - - it('should initialise the controller model when created', function() { - var ctrl = createController(); - expect(ctrl.model.name).toEqual(''); - expect(ctrl.model.public).toEqual(false); - }); - }); -})(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.html b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.html deleted file mode 100644 index 773cef2882..0000000000 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container-modal.html +++ /dev/null @@ -1,73 +0,0 @@ -
- - - - - -
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container.help.html b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container.help.html new file mode 100644 index 0000000000..443ee03f12 --- /dev/null +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/create-container.help.html @@ -0,0 +1,11 @@ +
+

+ A container is a storage compartment for your data and provides a way for + you to organize your data. You can think of a container as a folder in + Windows® or a directory in UNIX®. The primary difference between a + container and these other file system concepts is that containers cannot be + nested. You can, however, create an unlimited number of containers within + your account. Data must be stored in a container so you must have at least + one container defined in your account prior to uploading data. +

+
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 25d249b5f4..eb4688ba36 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 @@ -110,14 +110,19 @@ * @description * Get the container's detailed metadata * + * If you just wish to test for the existence of the container, set + * ignoreError so user-visible error isn't automatically displayed. * @returns {Object} An object with the metadata fields. * */ - function getContainer(container) { - return apiService.get(service.getContainerURL(container) + '/metadata/') - .error(function() { - toastService.add('error', gettext('Unable to get the container details.')); - }); + function getContainer(container, ignoreError) { + var promise = apiService.get(service.getContainerURL(container) + '/metadata/'); + if (ignoreError) { + return promise.error(angular.noop); + } + return promise.error(function() { + toastService.add('error', gettext('Unable to get the container details.')); + }); } /**