Merge "Migrate Create Container to schema form"

This commit is contained in:
Jenkins 2016-12-02 19:05:35 +00:00 committed by Gerrit Code Review
commit 93b1cef050
8 changed files with 202 additions and 171 deletions

View File

@ -317,3 +317,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

View File

@ -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});
}
);
}

View File

@ -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() {

View File

@ -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};
}
})();

View File

@ -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);
});
});
})();

View File

@ -1,73 +0,0 @@
<div ng-form="containerForm">
<div class="modal-header ui-draggable-handle">
<button type="button" class="close" ng-click="$dismiss()" aria-hidden="true" aria-label="Close">
<span aria-hidden="true" class="fa fa-times"></span>
</button>
<div class="h3 modal-title" translate>Create Container</div>
</div>
<div class="modal-body">
<div class="row">
<div class="col-sm-6">
<fieldset>
<div class="form-group required"
ng-class="{'has-error': containerForm.name.$invalid && containerForm.name.$dirty}">
<label class="control-label required" for="id_name" translate>Container Name</label>
<span class="hz-icon-required fa fa-asterisk"></span>
<div>
<input class="form-control" id="id_name" ng-model="ctrl.model.name" maxlength="255"
name="name" type="text" ng-required="true" pattern="[^/]+">
</div>
<span class="help-block" ng-show="containerForm.name.$error.pattern" translate>
Container name must not contain "/".
</span>
<span class="help-block"
ng-show="containerForm.name.$error.required && containerForm.name.$dirty" translate>
A name is required for your container.
</span>
</div>
<div class="form-group">
<label class="control-label" translate>Container Access</label>
<div class="themable-checkbox">
<input type="checkbox" ng-model="ctrl.model.public"
name="public" checked id="id_public">
<label for="id_public">
<span translate>Public</span>
</label>
</div>
</div>
</fieldset>
</div>
<div class="col-sm-6">
<p translate>
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&reg; or a directory in UNIX&reg;. 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.
</p>
<p translate>
Note: A Public Container will allow anyone with the Public URL to
gain access to your objects in the container.
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default" ng-click="$dismiss()">
<span class="fa fa-close"></span>
<translate>Cancel</translate>
</button>
<button class="btn btn-primary"
ng-click="$close(ctrl.model)"
ng-disabled="containerForm.$invalid">
<span class="fa fa-plus"></span>
<translate>Create</translate>
</button>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="col-sm-6">
<p translate>
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&reg; or a directory in UNIX&reg;. 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.
</p>
</div>

View File

@ -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.'));
});
}
/**