Merge "Migrate Create Container to schema form"
This commit is contained in:
commit
93b1cef050
@ -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
|
||||
|
@ -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});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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};
|
||||
}
|
||||
})();
|
@ -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);
|
||||
});
|
||||
});
|
||||
})();
|
@ -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® 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.
|
||||
</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>
|
@ -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® 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.
|
||||
</p>
|
||||
</div>
|
@ -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.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user