Merge "Add ngSwift container actions"

This commit is contained in:
Jenkins 2016-03-15 19:38:19 +00:00 committed by Gerrit Code Review
commit 7b9b908e6b
16 changed files with 677 additions and 64 deletions

View File

@ -18,6 +18,7 @@ from django import forms
from django.views.decorators.csrf import csrf_exempt
from django.views import generic
from horizon import exceptions
from openstack_dashboard import api
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
@ -74,7 +75,14 @@ class Container(generic.View):
if 'is_public' in request.DATA:
metadata['is_public'] = request.DATA['is_public']
api.swift.swift_create_container(request, container, metadata=metadata)
# This will raise an exception if the container already exists
try:
api.swift.swift_create_container(request, container,
metadata=metadata)
except exceptions.AlreadyExists as e:
# 409 Conflict
return rest_utils.JSONResponse(str(e), 409)
return rest_utils.CreatedResponse(
u'/api/swift/containers/%s' % container,
)

View File

@ -1,12 +1,23 @@
.hz-container-accordion {
cursor: pointer;
.accordion-toggle:hover {
text-decoration: none;
.accordion-toggle {
display: inline-block;
width: calc(100% - 1em);
padding: $panel-heading-padding;
position: relative;
&:hover {
text-decoration: none;
}
}
.panel-heading {
padding: 0;
}
.panel-body {
padding: 5px;
padding: $panel-body-padding;
ul {
padding: 0;
@ -18,13 +29,28 @@
padding: 0;
& > h4 > a {
padding: $panel-heading-padding;
padding: 0;
display: inline-block;
width: 100%;
& > div {
padding: $panel-heading-padding;
}
}
}
}
.hz-container-delete-icon {
font-size: 1em;
right: 0;
top: 0;
position: absolute;
padding: $panel-heading-padding;
&:hover {
color: $brand-danger;
}
}
.hz-container-title,
.hz-container-toggle {
&, &:hover {
@ -32,20 +58,45 @@
}
}
.hz-objects {
.page_title > th {
padding-top: 0;
}
.hz-container-title {
padding-right: .5em;
}
.hz-container-action {
padding-bottom: $padding-base-horizontal;
}
// specificity required
.hz-objects.table > thead > tr > .table_header {
padding-top: 0;
border: none;
}
.hz-object-path {
margin-bottom: 0;
padding-left: 0;
padding-top: 0;
background-color: inherit;
& > li {
&:nth-child(2):before {
content: ":";
}
}
}
}
.hz-object-detail {
.hz-object-label {
font-weight: bold;
&:after {
content: ':';
}
}
@media(min-width: $screen-lg) {
.hz-object-val {
padding-left: 0;
}
}
}

View File

@ -48,16 +48,19 @@
var model = {
info: {},
containers: [],
containerName: '',
container: null,
objects: [],
folder: '',
pseudo_folder_hierarchy: [],
DELIMETER: '/', // TODO where is this configured in the current panel
initialize: initialize,
selectContainer: selectContainer
selectContainer: selectContainer,
fetchContainerDetail: fetchContainerDetail
};
return model;
/**
* @ngdoc method
* @name ContainersModel.initialize
@ -78,8 +81,23 @@
);
}
/**
* @ngdoc method
* @name ContainersModel.selectContainer
* @returns {promise}
*
* @description
* Sets the currently active container and subfolder path, and
* fetches the object listing. Returns the promise for the object
* listing fetch.
*/
function selectContainer(name, folder) {
model.containerName = name;
for (var i = 0; i < model.containers.length; i++) {
if (model.containers[i].name === name) {
model.container = model.containers[i];
break;
}
}
model.objects.length = 0;
model.pseudo_folder_hierarchy.length = 0;
model.folder = folder;
@ -99,6 +117,36 @@
});
}
return model;
/**
* @ngdoc method
* @name ContainersModel.fetchContainerDetail
* @returns {promise}
*
* @description
* Fetch the detailed information about a container (beyond its name,
* contents count and byte size fetched in the containers listing).
*
* The timestamp will be converted from the ISO string to a Javascript Date.
*/
function fetchContainerDetail(container, force) {
// only fetch if we haven't already
if (container.is_fetched && !force) {
return;
}
swiftAPI.getContainer(container.name).then(
function success(response) {
// copy the additional detail into the container
angular.extend(container, response.data);
container.is_fetched = true;
// parse the timestamp for sensible display
var milliseconds = Date.parse(container.timestamp);
if (!isNaN(milliseconds)) {
container.timestamp = new Date(milliseconds);
}
}
);
}
}
})();

View File

@ -53,13 +53,14 @@
expect(service.containers).toEqual(['two', 'items']);
});
it('should load container contents', function test() {
it('should select containers and load contents', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getObjects').and.returnValue(deferred.promise);
service.containers = [{name: 'spam'}, {name: 'not spam'}];
service.selectContainer('spam');
expect(service.containerName).toEqual('spam');
expect(service.container.name).toEqual('spam');
expect(swiftAPI.getObjects).toHaveBeenCalledWith('spam', {delimiter: '/'});
deferred.resolve({data: {items: ['two', 'items']}});
@ -72,10 +73,11 @@
it('should load subfolder contents', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getObjects').and.returnValue(deferred.promise);
service.containers = [{name: 'spam'}];
service.selectContainer('spam', 'ham');
expect(service.containerName).toEqual('spam');
expect(service.container.name).toEqual('spam');
expect(service.folder).toEqual('ham');
expect(swiftAPI.getObjects).toHaveBeenCalledWith('spam', {path: 'ham/', delimiter: '/'});
@ -84,5 +86,63 @@
expect(service.objects).toEqual(['two', 'items']);
expect(service.pseudo_folder_hierarchy).toEqual(['ham']);
});
it('should fetch container detail', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise);
var container = {name: 'spam'};
service.fetchContainerDetail(container);
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam');
deferred.resolve({data: { info: 'yes!', timestamp: '2016-02-03T16:38:42.0Z' }});
$rootScope.$apply();
expect(container.info).toEqual('yes!');
expect(container.timestamp).toBeDefined();
expect(container.timestamp).toEqual(new Date(Date.UTC(2016, 1, 3, 16, 38, 42, 0)));
});
it('should handle bad timestamp data', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise);
var container = {name: 'spam'};
service.fetchContainerDetail(container);
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam');
deferred.resolve({data: { info: 'yes!', timestamp: 'b0rken' }});
$rootScope.$apply();
expect(container.info).toEqual('yes!');
expect(container.timestamp).toBeDefined();
expect(container.timestamp).toEqual('b0rken');
});
it('should not re-fetch container detail', function test() {
spyOn(swiftAPI, 'getContainer');
var container = {name: 'spam', is_fetched: true};
service.fetchContainerDetail(container);
expect(swiftAPI.getContainer).not.toHaveBeenCalled();
expect(container.info).toBeUndefined();
});
it('should not re-fetch container detail unless forced', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise);
var container = {name: 'spam', is_fetched: true};
service.fetchContainerDetail(container, true);
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam');
deferred.resolve({data: { info: 'yes!', timestamp: 'b0rken' }});
$rootScope.$apply();
expect(container.info).toEqual('yes!');
});
});
})();

View File

@ -1,5 +1,5 @@
/*
* (c) Copyright 2015 Rackspace US, Inc
* (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.
@ -30,21 +30,121 @@
.controller('horizon.dashboard.project.containers.ContainersController', ContainersController);
ContainersController.$inject = [
'horizon.app.core.openstack-service-api.swift',
'horizon.dashboard.project.containers.containers-model',
'horizon.dashboard.project.containers.basePath',
'horizon.dashboard.project.containers.baseRoute',
'horizon.dashboard.project.containers.containerRoute',
'$location'
'horizon.framework.widgets.modal.simple-modal.service',
'horizon.framework.widgets.toast.service',
'$location',
'$modal'
];
function ContainersController(containersModel, containerRoute, $location) {
function ContainersController(swiftAPI, containersModel, basePath, baseRoute, containerRoute,
simpleModalService, toastService, $location, $modal)
{
var ctrl = this;
ctrl.model = containersModel;
containersModel.initialize();
ctrl.baseRoute = baseRoute;
ctrl.containerRoute = containerRoute;
ctrl.selectedContainer = '';
ctrl.selectContainer = function (name) {
ctrl.selectedContainer = name;
$location.path(containerRoute + name);
};
ctrl.toggleAccess = toggleAccess;
ctrl.deleteContainer = deleteContainer;
ctrl.deleteContainerAction = deleteContainerAction;
ctrl.createContainer = createContainer;
ctrl.createContainerAction = createContainerAction;
ctrl.selectContainer = selectContainer;
//////////
function selectContainer(container) {
ctrl.model.fetchContainerDetail(container);
ctrl.selectedContainer = container.name;
$location.path(ctrl.containerRoute + container.name);
}
function toggleAccess(container) {
swiftAPI.setContainerAccess(container.name, container.is_public).then(
function updated() {
var access = 'private';
if (container.is_public) {
access = 'public';
}
toastService.add('success', interpolate(
gettext('Container %(name)s is now %(access)s.'),
{name: container.name, access: access},
true
));
// re-fetch container details
ctrl.model.fetchContainerDetail(container, true);
},
function failure() {
container.is_public = !container.is_public;
});
}
function deleteContainer(container) {
var options = {
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')
};
simpleModalService.modal(options).result.then(function confirmed() {
return ctrl.deleteContainerAction(container);
});
}
function deleteContainerAction(container) {
swiftAPI.deleteContainer(container.name).then(
function deleted() {
toastService.add('success', interpolate(
gettext('Container %(name)s deleted.'), container, true
));
// remove the deleted container from the containers list
for (var i = ctrl.model.containers.length - 1; i >= 0; i--) {
if (ctrl.model.containers[i].name === container.name) {
ctrl.model.containers.splice(i, 1);
break;
}
}
// route back to no selected container if we deleted the current one
if (ctrl.selectedContainer === container.name) {
$location.path(ctrl.baseRoute);
}
});
}
function createContainer() {
var localSpec = {
backdrop: 'static',
controller: 'CreateContainerModalController as ctrl',
templateUrl: basePath + 'create-container-modal.html'
};
$modal.open(localSpec).result.then(function create(result) {
return ctrl.createContainerAction(result);
});
}
function createContainerAction(result) {
swiftAPI.createContainer(result.name, result.public).then(
function success() {
toastService.add('success', interpolate(
gettext('Container %(name)s created.'), result, true
));
// generate a table row with no contents
ctrl.model.containers.push({name: result.name, count: 0, bytes: 0});
}
);
}
}
})();

View File

@ -22,17 +22,47 @@
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.dashboard.project'));
var $location, controller, model;
var fakeModel = {
loadContainerContents: angular.noop,
initialize: angular.noop,
fetchContainerDetail: function fake() {
return {
then: function then(callback) {
callback({
name: 'spam',
is_public: true
});
}
};
}
};
beforeEach(inject(function ($injector) {
var $q, $modal, $location, $rootScope, controller, 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_) {
controller = $injector.get('$controller');
$q = _$q_;
$location = $injector.get('$location');
model = $injector.get('horizon.dashboard.project.containers.containers-model');
$modal = _$modal_;
$rootScope = _$rootScope_;
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');
spyOn(fakeModel, 'initialize');
spyOn(fakeModel, 'loadContainerContents');
spyOn(fakeModel, 'fetchContainerDetail').and.callThrough();
spyOn(toast, 'add');
}));
function createController() {
return controller(
'horizon.dashboard.project.containers.ContainersController', {
'horizon.dashboard.project.containers.baseRoute': 'base ham',
'horizon.dashboard.project.containers.containerRoute': 'eggs '
});
}
@ -43,17 +73,145 @@
});
it('should invoke initialise the model when created', function() {
spyOn(model, 'initialize');
createController();
expect(model.initialize).toHaveBeenCalled();
expect(fakeModel.initialize).toHaveBeenCalled();
});
it('should update current container name when one is selected', function () {
spyOn($location, 'path');
var ctrl = createController();
ctrl.selectContainer('and spam');
ctrl.selectContainer({name: 'and spam'});
expect($location.path).toHaveBeenCalledWith('eggs and spam');
expect(ctrl.selectedContainer).toEqual('and spam');
expect(fakeModel.fetchContainerDetail).toHaveBeenCalledWith({name: 'and spam'});
});
it('should set container to public', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'setContainerAccess').and.returnValue(deferred.promise);
var ctrl = createController();
var container = {name: 'spam', is_public: true};
ctrl.toggleAccess(container);
expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', true);
deferred.resolve();
$rootScope.$apply();
expect(toast.add).toHaveBeenCalledWith('success', 'Container spam is now public.');
expect(fakeModel.fetchContainerDetail).toHaveBeenCalledWith(container, true);
});
it('should set container to private', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'setContainerAccess').and.returnValue(deferred.promise);
var ctrl = createController();
var container = {name: 'spam', is_public: false};
ctrl.toggleAccess(container);
expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', false);
deferred.resolve();
$rootScope.$apply();
expect(toast.add).toHaveBeenCalledWith('success', 'Container spam is now private.');
expect(fakeModel.fetchContainerDetail).toHaveBeenCalledWith(container, true);
});
it('should open a dialog for delete confirmation', function test() {
// fake promise to poke at later
var deferred = $q.defer();
var result = { result: deferred.promise };
spyOn(simpleModal, 'modal').and.returnValue(result);
var ctrl = createController();
spyOn(ctrl, 'deleteContainerAction');
ctrl.deleteContainer({name: 'spam', is_public: true});
// ensure modal is constructed correctly.
expect(simpleModal.modal).toHaveBeenCalled();
var spec = simpleModal.modal.calls.mostRecent().args[0];
expect(spec.title).toBeDefined();
expect(spec.body).toBeDefined();
expect(spec.submit).toBeDefined();
expect(spec.cancel).toBeDefined();
// when the modal is resolved, make sure delete is called
deferred.resolve();
$rootScope.$apply();
expect(ctrl.deleteContainerAction).toHaveBeenCalledWith({name: 'spam', is_public: true});
});
it('should delete containers', function test() {
fakeModel.containers = [{name: 'one'}, {name: 'two'}, {name: 'three'}];
var deferred = $q.defer();
spyOn(swiftAPI, 'deleteContainer').and.returnValue(deferred.promise);
spyOn($location, 'path');
var ctrl = createController();
ctrl.selectedContainer = 'one';
createController().deleteContainerAction(fakeModel.containers[1]);
deferred.resolve();
$rootScope.$apply();
expect(toast.add).toHaveBeenCalledWith('success', 'Container two deleted.');
expect(fakeModel.containers[0].name).toEqual('one');
expect(fakeModel.containers[1].name).toEqual('three');
expect(fakeModel.containers.length).toEqual(2);
expect($location.path).not.toHaveBeenCalled();
});
it('should reset the location when the current container is deleted', function test() {
fakeModel.containers = [{name: 'one'}, {name: 'two'}, {name: 'three'}];
var deferred = $q.defer();
spyOn(swiftAPI, 'deleteContainer').and.returnValue(deferred.promise);
spyOn($location, 'path');
var ctrl = createController();
ctrl.selectedContainer = 'two';
ctrl.deleteContainerAction(fakeModel.containers[1]);
deferred.resolve();
$rootScope.$apply();
expect($location.path).toHaveBeenCalledWith('base ham');
});
it('should open a dialog for creation', function test() {
var deferred = $q.defer();
var result = { result: deferred.promise };
spyOn($modal, 'open').and.returnValue(result);
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();
// when the modal is resolved, make sure delete is called
deferred.resolve('spam');
$rootScope.$apply();
expect(ctrl.createContainerAction).toHaveBeenCalledWith('spam');
});
it('should create containers', function test() {
fakeModel.containers = [];
var deferred = $q.defer();
spyOn(swiftAPI, 'createContainer').and.returnValue(deferred.promise);
createController().createContainerAction({name: 'spam', public: true});
deferred.resolve();
$rootScope.$apply();
expect(toast.add).toHaveBeenCalledWith('success', 'Container spam created.');
expect(fakeModel.containers[0].name).toEqual('spam');
expect(fakeModel.containers.length).toEqual(1);
});
});
})();

View File

@ -1,27 +1,70 @@
<div id="containers_wrapper" ng-controller="horizon.dashboard.project.containers.ContainersController as cc">
<div class="col-md-3">
<accordion class="hz-container-accordion">
<accordion-group ng-repeat="container in cc.model.containers track by container.name"
ng-class="{'panel-primary': container.name === cc.selectedContainer}"
ng-click="cc.selectContainer(container.name)">
<accordion-heading>
<span class="hz-container-title truncate" title="{$ container.name $}">
{$ container.name $}
</span>
</accordion-heading>
<div class="row hz-container-actions">
<div class="col-xs-12 hz-container-action">
<button type="button" class="btn btn-default" ng-click="cc.createContainer()">
<span class="fa fa-plus"></span>
<translate>Container</translate>
</button>
</div>
</div>
<ul>
<li><translate>Object Count</translate>: {$container.container_object_count$}</li>
<li><translate>Size</translate>: {$container.container_bytes_used | bytes$}</li>
<li>
<translate>Access</translate>:
<span ng-if="container.is_public"><translate>Public</translate></span>
<span ng-if="!container.is_public"><translate>Private</translate></span>
</li>
<li><translate>Timestamp</translate>: {$container.timestamp$}</li>
</ul>
</accordion-group>
</accordion>
<div class="row">
<div class="col-xs-12">
<accordion class="hz-container-accordion">
<accordion-group ng-repeat="container in cc.model.containers track by container.name"
ng-class="{'panel-primary': container.name === cc.selectedContainer}">
<accordion-heading>
<div ng-click="cc.selectContainer(container)">
<span class="hz-container-title truncate"
tooltip="{$ container.name $}" tooltip-placement="top"
tooltip-popup-delay="500" tooltip-trigger="mouseenter">
{$ container.name $}
</span>
<span tooltip="{$ 'Delete Container' | translate $}" tooltip-placement="top"
tooltip-trigger="mouseenter"
class="fa fa-trash hz-container-delete-icon"
ng-if="container.name === cc.selectedContainer"
ng-click="cc.deleteContainer(container); $event.stopPropagation()"></span>
</div>
</accordion-heading>
<div ng-if="!container.is_fetched" class="horizon-pending-bar container-pending-bar">
<div class="progress progress-striped active">
<div class="progress-bar" style="width: 100%"></div>
</div>
</div>
<ul ng-if="container.is_fetched" class="hz-object-detail">
<li class="hz-object-count row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Object Count</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.count$}</span>
</li>
<li class="hz-object-size row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Size</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.bytes | bytes$}</span>
</li>
<li class="hz-object-timestamp row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Date Created</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.timestamp | date$}</span>
</li>
<li class="hz-object-link row">
<div class="themable-checkbox col-lg-7 col-md-12">
<input type="checkbox" id="id_access" ng-model="container.is_public"
ng-if="container.name === cc.selectedContainer"
ng-click="cc.toggleAccess(container)">
<label class="hz-object-label" for="id_access" translate>Public Access</label>
</div>
<span class="hz-object-val col-lg-5 col-md-12">
<a href="{$ container.public_url $}" target="_blank"
ng-show="container.public_url" translate>link</a>
<span ng-hide="container.public_url" translate>disabled</span>
</span>
</li>
</ul>
</accordion-group>
</accordion>
</div>
</div>
</div>
<div class="col-md-9">

View File

@ -44,6 +44,8 @@
$provide.constant('horizon.dashboard.project.containers.basePath', path);
var baseRoute = $windowProvider.$get().WEBROOT + 'project/ngcontainers/';
$provide.constant('horizon.dashboard.project.containers.baseRoute', baseRoute);
var containerRoute = baseRoute + 'container/';
$provide.constant('horizon.dashboard.project.containers.containerRoute', containerRoute);
@ -51,10 +53,10 @@
.when(baseRoute, {
templateUrl: path + 'select-container.html'
})
.when(containerRoute + ':containerName', {
.when(containerRoute + ':container', {
templateUrl: path + 'objects.html'
})
.when(containerRoute + ':containerName/:folder*', {
.when(containerRoute + ':container/:folder*', {
templateUrl: path + 'objects.html'
});
}

View File

@ -17,14 +17,16 @@
'use strict';
describe('horizon.dashboard.project.containers.containerRoute constant', function () {
var containerRoute;
var baseRoute, containerRoute;
beforeEach(module('horizon.dashboard.project.containers'));
beforeEach(inject(function ($injector) {
baseRoute = $injector.get('horizon.dashboard.project.containers.baseRoute');
containerRoute = $injector.get('horizon.dashboard.project.containers.containerRoute');
}));
it('should be defined', function () {
it('should define routes', function () {
expect(baseRoute).toBeDefined();
expect(containerRoute).toBeDefined();
});
});

View File

@ -0,0 +1,27 @@
/*
* (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

@ -0,0 +1,40 @@
/**
* (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

@ -0,0 +1,70 @@
<div ng-form="containerForm">
<div class="modal-header ui-draggable-handle">
<a href="#" class="close" ng-click="$dismiss()">
<span class="fa fa-times"></span>
</a>
<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="[^/]+"
placeholder="{$ 'Required' | translate $}">
</div>
<span class="help-block" ng-show="containerForm.name.$error.pattern" translate>
Container name must not contain "/".
</span>
</div>
<div class="form-group">
<label class="control-label" translate>Container Access</label>
<div>
<label for="id_public" translate>
<input type="checkbox" ng-model="ctrl.model.public"
name="public" checked id="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 secondary" 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

@ -40,13 +40,13 @@
ctrl.model = containersModel;
ctrl.containerURL = containerRoute + $routeParams.containerName + '/';
ctrl.containerURL = containerRoute + $routeParams.container + '/';
if (angular.isDefined($routeParams.folder)) {
ctrl.currentURL = ctrl.containerURL + $routeParams.folder + '/';
} else {
ctrl.currentURL = ctrl.containerURL;
}
ctrl.model.selectContainer($routeParams.containerName, $routeParams.folder);
ctrl.model.selectContainer($routeParams.container, $routeParams.folder);
}
})();

View File

@ -41,7 +41,7 @@
it('should load contents', function test () {
spyOn(model, 'selectContainer');
$routeParams.containerName = 'spam';
$routeParams.container = 'spam';
var ctrl = createController();
expect(ctrl.containerURL).toEqual('eggs/spam/');
@ -52,7 +52,7 @@
it('should handle subfolders', function test () {
spyOn(model, 'selectContainer');
$routeParams.containerName = 'spam';
$routeParams.container = 'spam';
$routeParams.folder = 'ham';
var ctrl = createController();

View File

@ -4,10 +4,10 @@
hz-table default-sort="name">
<thead>
<tr class="page_title table_caption">
<th colspan="4">
<th class="table_header" colspan="4">
<ol class="breadcrumb hz-object-path">
<li class="h4">
<a ng-href="{$ oc.containerURL $}">{$ oc.model.containerName $}</a>
<a ng-href="{$ oc.containerURL $}">{$ oc.model.container.name $}</a>
</li>
<li ng-repeat="pf in oc.model.pseudo_folder_hierarchy track by $index" ng-class="{'active':$last}">
<span>
@ -21,7 +21,7 @@
</tr>
<tr class="table_caption">
<th colspan="4" class="search-header">
<th colspan="4" class="table_header search-header">
<hz-search-bar group-classes="input-group-sm"
icon-classes="fa-search" input-classes="form-control" placeholder="Filter">
</hz-search-bar>

View File

@ -132,8 +132,12 @@
data.is_public = true;
}
return apiService.post(service.getContainerURL(container) + '/metadata/', data)
.error(function () {
toastService.add('error', gettext('Unable to create the container.'));
.error(function (response, status) {
if (status === 409) {
toastService.add('error', response);
} else {
toastService.add('error', gettext('Unable to create the container.'));
}
});
}