Merge "Add ngSwift container actions"
This commit is contained in:
commit
7b9b908e6b
@ -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,
|
||||
)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -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!');
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -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});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -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">
|
||||
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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};
|
||||
}
|
||||
})();
|
@ -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);
|
||||
});
|
||||
});
|
||||
})();
|
@ -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® 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 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>
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user