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`_.
|
read more in the `SASS documentation`_.
|
||||||
|
|
||||||
.. _SASS documentation: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#import
|
.. _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.basePath',
|
||||||
'horizon.dashboard.project.containers.baseRoute',
|
'horizon.dashboard.project.containers.baseRoute',
|
||||||
'horizon.dashboard.project.containers.containerRoute',
|
'horizon.dashboard.project.containers.containerRoute',
|
||||||
|
'horizon.framework.widgets.form.ModalFormService',
|
||||||
'horizon.framework.widgets.modal.simple-modal.service',
|
'horizon.framework.widgets.modal.simple-modal.service',
|
||||||
'horizon.framework.widgets.toast.service',
|
'horizon.framework.widgets.toast.service',
|
||||||
'$location',
|
'$location',
|
||||||
'$modal'
|
'$q'
|
||||||
];
|
];
|
||||||
|
|
||||||
function ContainersController(swiftAPI,
|
function ContainersController(swiftAPI,
|
||||||
@ -46,16 +47,18 @@
|
|||||||
basePath,
|
basePath,
|
||||||
baseRoute,
|
baseRoute,
|
||||||
containerRoute,
|
containerRoute,
|
||||||
|
modalFormService,
|
||||||
simpleModalService,
|
simpleModalService,
|
||||||
toastService,
|
toastService,
|
||||||
$location,
|
$location,
|
||||||
$modal) {
|
$q) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
ctrl.model = containersModel;
|
ctrl.model = containersModel;
|
||||||
ctrl.model.initialize();
|
ctrl.model.initialize();
|
||||||
ctrl.baseRoute = baseRoute;
|
ctrl.baseRoute = baseRoute;
|
||||||
ctrl.containerRoute = containerRoute;
|
ctrl.containerRoute = containerRoute;
|
||||||
|
|
||||||
|
ctrl.checkContainerNameConflict = checkContainerNameConflict;
|
||||||
ctrl.toggleAccess = toggleAccess;
|
ctrl.toggleAccess = toggleAccess;
|
||||||
ctrl.deleteContainer = deleteContainer;
|
ctrl.deleteContainer = deleteContainer;
|
||||||
ctrl.deleteContainerAction = deleteContainerAction;
|
ctrl.deleteContainerAction = deleteContainerAction;
|
||||||
@ -64,6 +67,18 @@
|
|||||||
ctrl.selectContainer = selectContainer;
|
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) {
|
function selectContainer(container) {
|
||||||
ctrl.model.container = container;
|
ctrl.model.container = container;
|
||||||
@ -97,7 +112,7 @@
|
|||||||
title: gettext('Confirm Delete'),
|
title: gettext('Confirm Delete'),
|
||||||
body: interpolate(
|
body: interpolate(
|
||||||
gettext('Are you sure you want to delete container %(name)s?'), container, true
|
gettext('Are you sure you want to delete container %(name)s?'), container, true
|
||||||
),
|
),
|
||||||
submit: gettext('Yes'),
|
submit: gettext('Yes'),
|
||||||
cancel: gettext('No')
|
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() {
|
function createContainer() {
|
||||||
var localSpec = {
|
var model = {name: '', public: false};
|
||||||
backdrop: 'static',
|
var config = {
|
||||||
controller: 'CreateContainerModalController as ctrl',
|
title: gettext('Create Container'),
|
||||||
templateUrl: basePath + 'create-container-modal.html'
|
schema: createContainerSchema,
|
||||||
|
form: createContainerForm,
|
||||||
|
model: model
|
||||||
};
|
};
|
||||||
$modal.open(localSpec).result.then(function create(result) {
|
return modalFormService.open(config).then(function then() {
|
||||||
return ctrl.createContainerAction(result);
|
return ctrl.createContainerAction(model);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContainerAction(result) {
|
function createContainerAction(model) {
|
||||||
swiftAPI.createContainer(result.name, result.public).then(
|
return swiftAPI.createContainer(model.name, model.public).then(
|
||||||
function success() {
|
function success() {
|
||||||
toastService.add('success', interpolate(
|
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
|
// 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) {
|
beforeEach(module('horizon.dashboard.project.containers', function($provide) {
|
||||||
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
|
$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');
|
controller = $injector.get('$controller');
|
||||||
$q = _$q_;
|
$q = _$q_;
|
||||||
$location = $injector.get('$location');
|
$location = $injector.get('$location');
|
||||||
$modal = _$modal_;
|
|
||||||
$rootScope = _$rootScope_;
|
$rootScope = _$rootScope_;
|
||||||
|
modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService');
|
||||||
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
|
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
|
||||||
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
||||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||||
@ -180,24 +180,69 @@
|
|||||||
|
|
||||||
it('should open a dialog for creation', function test() {
|
it('should open a dialog for creation', function test() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
var result = { result: deferred.promise };
|
spyOn(modalFormService, 'open').and.returnValue(deferred.promise);
|
||||||
spyOn($modal, 'open').and.returnValue(result);
|
|
||||||
|
|
||||||
var ctrl = createController();
|
var ctrl = createController();
|
||||||
spyOn(ctrl, 'createContainerAction');
|
spyOn(ctrl, 'createContainerAction');
|
||||||
|
|
||||||
ctrl.createContainer();
|
ctrl.createContainer();
|
||||||
|
|
||||||
expect($modal.open).toHaveBeenCalled();
|
expect(modalFormService.open).toHaveBeenCalled();
|
||||||
var spec = $modal.open.calls.mostRecent().args[0];
|
var config = modalFormService.open.calls.mostRecent().args[0];
|
||||||
expect(spec.backdrop).toBeDefined();
|
expect(config.model).toBeDefined();
|
||||||
expect(spec.controller).toEqual('CreateContainerModalController as ctrl');
|
expect(config.schema).toBeDefined();
|
||||||
expect(spec.templateUrl).toBeDefined();
|
expect(config.form).toBeDefined();
|
||||||
|
|
||||||
// when the modal is resolved, make sure delete is called
|
// when the modal is resolved, make sure create is called
|
||||||
deferred.resolve('spam');
|
deferred.resolve();
|
||||||
$rootScope.$apply();
|
$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() {
|
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
|
* @description
|
||||||
* Get the container's detailed metadata
|
* 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.
|
* @returns {Object} An object with the metadata fields.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function getContainer(container) {
|
function getContainer(container, ignoreError) {
|
||||||
return apiService.get(service.getContainerURL(container) + '/metadata/')
|
var promise = apiService.get(service.getContainerURL(container) + '/metadata/');
|
||||||
.error(function() {
|
if (ignoreError) {
|
||||||
toastService.add('error', gettext('Unable to get the container details.'));
|
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