Add ngSwift containers and objects display

This patch adds listing of containers and objects in selected
containers, but not additional functionality. That additional
functionality will be added in subsequent patches.

To test set DISABLED = False in _1921_project_ng_containers_panel.py

Change-Id: I37980a7b84dbddb99d8f1d4d8235cc11917da30e
Co-Author: Neill Cox <neill@ingenious.com.au>
Partially-Implements: blueprint angularize-swift
This commit is contained in:
Richard Jones 2015-12-17 13:54:31 +11:00
parent b09f0e8c63
commit 4c39136997
20 changed files with 673 additions and 13 deletions

View File

@ -2,10 +2,14 @@
{% load i18n %}
{% block title %}{% trans "Containers" %}{% endblock %}
{% block ng_route_base %}
<base href="{{ WEBROOT }}">
{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Containers") %}
{% endblock page_header %}
{% block main %}
[This content to be replaced in follow-on patch with actual interface.]
<ng-include src="'{{ STATIC_URL }}dashboard/project/containers/containers.html'"></ng-include>
{% endblock %}

View File

@ -12,15 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.conf.urls import patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.project.ngcontainers import views
VIEW_MOD = 'openstack_dashboard.dashboards.project.ngcontainers.views'
urlpatterns = patterns(
'openstack_dashboard.dashboards.project.ngcontainers.views',
url(r'^$', views.IndexView.as_view(), name='index'),
)
urlpatterns = [
url(r'^(container/(?P<container_name>.+?)/(?P<subfolder_path>(.+/)+)?)?$',
views.IndexView.as_view(), name='index')
]

View File

@ -0,0 +1,51 @@
.hz-container-accordion {
cursor: pointer;
.accordion-toggle:hover {
text-decoration: none;
}
.panel-body {
padding: 5px;
ul {
padding: 0;
}
}
// have the toggle <a> fill the whole heading to make it clickable
.panel-heading {
padding: 0;
& > h4 > a {
padding: $panel-heading-padding;
display: inline-block;
width: 100%;
}
}
}
.hz-container-title,
.hz-container-toggle {
&, &:hover {
cursor: pointer;
}
}
.hz-objects {
.page_title > th {
padding-top: 0;
}
}
.hz-object-path {
margin-bottom: 0;
padding-left: 0;
padding-top: 0;
& > li {
&:nth-child(2):before {
content: ":";
}
}
}

View File

@ -0,0 +1,104 @@
/*
* (c) Copyright 2015 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';
var push = Array.prototype.push;
/**
* @ngdoc overview
* @name horizon.dashboard.project.containers
*
* @description
* Provide a model for the display of containers.
*/
angular
.module('horizon.dashboard.project.containers')
.factory('horizon.dashboard.project.containers.containers-model', ContainersModel);
ContainersModel.$inject = [
'horizon.app.core.openstack-service-api.swift',
'$q'
];
/**
* @ngdoc service
* @name ContainersModel
*
* @description
* This is responsible for providing data to the containers
* interface. It is also the center point of communication
* between the UI and services API.
*/
function ContainersModel(swiftAPI, $q) {
var model = {
info: {},
containers: [],
containerName: '',
objects: [],
folder: '',
pseudo_folder_hierarchy: [],
DELIMETER: '/', // TODO where is this configured in the current panel
initialize: initialize,
selectContainer: selectContainer
};
/**
* @ngdoc method
* @name ContainersModel.initialize
* @returns {promise}
*
* @description
* Send request to get data to initialize the model.
*/
function initialize() {
return $q.all(
swiftAPI.getContainers().then(function onContainers(data) {
model.containers.length = 0;
push.apply(model.containers, data.data.items);
}),
swiftAPI.getInfo().then(function onInfo(data) {
model.swift_info = data.info;
})
);
}
function selectContainer(name, folder) {
model.containerName = name;
model.objects.length = 0;
model.pseudo_folder_hierarchy.length = 0;
model.folder = folder;
var spec = {
delimiter: model.DELIMETER
};
if (folder) {
spec.path = folder + model.DELIMETER;
}
return swiftAPI.getObjects(name, spec).then(function onObjects(response) {
push.apply(model.objects, response.data.items);
if (folder) {
push.apply(model.pseudo_folder_hierarchy, folder.split(model.DELIMETER) || [folder]);
}
});
}
return model;
}
})();

View File

@ -0,0 +1,88 @@
/*
* (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 model', function() {
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.dashboard.project.containers'));
var service, $q, $rootScope, swiftAPI;
beforeEach(inject(function inject($injector, _$q_, _$rootScope_) {
service = $injector.get('horizon.dashboard.project.containers.containers-model');
$q = _$q_;
$rootScope = _$rootScope_;
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
}));
it('should initialise the model', function test() {
expect(service.containers).toBeDefined();
expect(service.DELIMETER).toBeDefined();
});
it('should retrieve the swift info and user containers on initalize()', function test() {
var infoDeferred = $q.defer();
spyOn(swiftAPI, 'getInfo').and.returnValue(infoDeferred.promise);
var containersDeferred = $q.defer();
spyOn(swiftAPI, 'getContainers').and.returnValue(containersDeferred.promise);
service.initialize();
expect(swiftAPI.getInfo).toHaveBeenCalled();
expect(swiftAPI.getContainers).toHaveBeenCalled();
infoDeferred.resolve({info: 'spam'});
containersDeferred.resolve({data: {items: ['two', 'items']}});
$rootScope.$apply();
expect(service.swift_info).toEqual('spam');
expect(service.containers).toEqual(['two', 'items']);
});
it('should load container contents', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getObjects').and.returnValue(deferred.promise);
service.selectContainer('spam');
expect(service.containerName).toEqual('spam');
expect(swiftAPI.getObjects).toHaveBeenCalledWith('spam', {delimiter: '/'});
deferred.resolve({data: {items: ['two', 'items']}});
$rootScope.$apply();
expect(service.objects).toEqual(['two', 'items']);
expect(service.pseudo_folder_hierarchy).toEqual([]);
});
it('should load subfolder contents', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getObjects').and.returnValue(deferred.promise);
service.selectContainer('spam', 'ham');
expect(service.containerName).toEqual('spam');
expect(service.folder).toEqual('ham');
expect(swiftAPI.getObjects).toHaveBeenCalledWith('spam', {path: 'ham/', delimiter: '/'});
deferred.resolve({data: {items: ['two', 'items']}});
$rootScope.$apply();
expect(service.objects).toEqual(['two', 'items']);
expect(service.pseudo_folder_hierarchy).toEqual(['ham']);
});
});
})();

View File

@ -0,0 +1,50 @@
/*
* (c) Copyright 2015 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';
/**
* @ngdoc controller
*
* @name horizon.dashboard.project.containers.ContainersController
*
* @description
* Controller for the interface around a list of containers for a single account.
*/
angular
.module('horizon.dashboard.project.containers')
.controller('horizon.dashboard.project.containers.ContainersController', ContainersController);
ContainersController.$inject = [
'horizon.dashboard.project.containers.containers-model',
'horizon.dashboard.project.containers.containerRoute',
'$location'
];
function ContainersController(containersModel, containerRoute, $location) {
var ctrl = this;
ctrl.model = containersModel;
containersModel.initialize();
ctrl.containerRoute = containerRoute;
ctrl.selectedContainer = '';
ctrl.selectContainer = function (name) {
ctrl.selectedContainer = name;
$location.path(containerRoute + name);
};
}
})();

View File

@ -0,0 +1,59 @@
/**
* (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 containers controller', function() {
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.dashboard.project'));
var $location, controller, model;
beforeEach(inject(function ($injector) {
controller = $injector.get('$controller');
$location = $injector.get('$location');
model = $injector.get('horizon.dashboard.project.containers.containers-model');
}));
function createController() {
return controller(
'horizon.dashboard.project.containers.ContainersController', {
'horizon.dashboard.project.containers.containerRoute': 'eggs '
});
}
it('should set containerRoute', function() {
var ctrl = createController();
expect(ctrl.containerRoute).toBeDefined();
});
it('should invoke initialise the model when created', function() {
spyOn(model, 'initialize');
createController();
expect(model.initialize).toHaveBeenCalled();
});
it('should update current container name when one is selected', function () {
spyOn($location, 'path');
var ctrl = createController();
ctrl.selectContainer('and spam');
expect($location.path).toHaveBeenCalledWith('eggs and spam');
expect(ctrl.selectedContainer).toEqual('and spam');
});
});
})();

View File

@ -0,0 +1,30 @@
<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>
<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>
<div class="col-md-9">
<div ng-view class="objects_wrapper"></div>
</div>
</div>

View File

@ -0,0 +1,61 @@
/**
* (c) Copyright 2015 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';
/**
* @ngdoc overview
* @ngname horizon.dashboard.project.containers
*
* @description
* Provides the services and widgets required
* to support and display the project containers panel.
*/
angular
.module('horizon.dashboard.project.containers', ['ngRoute'])
.config(config);
config.$inject = [
'$provide',
'$routeProvider',
'$windowProvider'
];
/**
* @name horizon.dashboard.project.containers.basePath
* @description Base path for the project dashboard
*/
function config($provide, $routeProvider, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'dashboard/project/containers/';
$provide.constant('horizon.dashboard.project.containers.basePath', path);
var baseRoute = $windowProvider.$get().WEBROOT + 'project/ngcontainers/';
var containerRoute = baseRoute + 'container/';
$provide.constant('horizon.dashboard.project.containers.containerRoute', containerRoute);
$routeProvider
.when(baseRoute, {
templateUrl: path + 'select-container.html'
})
.when(containerRoute + ':containerName', {
templateUrl: path + 'objects.html'
})
.when(containerRoute + ':containerName/:folder*', {
templateUrl: path + 'objects.html'
});
}
})();

View File

@ -0,0 +1,31 @@
/*
* (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.containerRoute constant', function () {
var containerRoute;
beforeEach(module('horizon.dashboard.project.containers'));
beforeEach(inject(function ($injector) {
containerRoute = $injector.get('horizon.dashboard.project.containers.containerRoute');
}));
it('should be defined', function () {
expect(containerRoute).toBeDefined();
});
});
})();

View File

@ -0,0 +1,52 @@
/*
* (c) Copyright 2015 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';
/**
* @ngdoc controller
*
* @name horizon.dashboard.project.containers.ObjectsController
*
* @description
* Controller for the interface around the objects in a single container.
*/
angular
.module('horizon.dashboard.project.containers')
.controller('horizon.dashboard.project.containers.ObjectsController', ObjectsController);
ObjectsController.$inject = [
'horizon.dashboard.project.containers.containers-model',
'horizon.dashboard.project.containers.containerRoute',
'$routeParams'
];
function ObjectsController(containersModel, containerRoute, $routeParams) {
var ctrl = this;
ctrl.model = containersModel;
ctrl.containerURL = containerRoute + $routeParams.containerName + '/';
if (angular.isDefined($routeParams.folder)) {
ctrl.currentURL = ctrl.containerURL + $routeParams.folder + '/';
} else {
ctrl.currentURL = ctrl.containerURL;
}
ctrl.model.selectContainer($routeParams.containerName, $routeParams.folder);
}
})();

View File

@ -0,0 +1,65 @@
/**
* (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 objects controller', function() {
var $routeParams, controller, model;
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.containers', function before($provide) {
$routeParams = {};
$provide.value('$routeParams', $routeParams);
}));
beforeEach(inject(function ($injector) {
controller = $injector.get('$controller');
model = $injector.get('horizon.dashboard.project.containers.containers-model');
}));
function createController() {
return controller('horizon.dashboard.project.containers.ObjectsController', {
'horizon.dashboard.project.containers.containerRoute': 'eggs/'
});
}
it('should load contents', function test () {
spyOn(model, 'selectContainer');
$routeParams.containerName = 'spam';
var ctrl = createController();
expect(ctrl.containerURL).toEqual('eggs/spam/');
expect(ctrl.currentURL).toEqual('eggs/spam/');
expect(model.selectContainer).toHaveBeenCalledWith('spam', undefined);
});
it('should handle subfolders', function test () {
spyOn(model, 'selectContainer');
$routeParams.containerName = 'spam';
$routeParams.folder = 'ham';
var ctrl = createController();
expect(ctrl.containerURL).toEqual('eggs/spam/');
expect(ctrl.currentURL).toEqual('eggs/spam/ham/');
expect(model.selectContainer).toHaveBeenCalledWith('spam', 'ham');
});
});
})();

View File

@ -0,0 +1,50 @@
<table class="table hz-objects table-hover table-striped"
ng-controller="horizon.dashboard.project.containers.ObjectsController as oc"
st-table="displayContents" st-safe-src="oc.model.objects"
hz-table default-sort="name">
<thead>
<tr class="page_title table_caption">
<th colspan="4">
<ol class="breadcrumb hz-object-path">
<li class="h4">
<a ng-href="{$ oc.containerURL $}">{$ oc.model.containerName $}</a>
</li>
<li ng-repeat="pf in oc.model.pseudo_folder_hierarchy track by $index" ng-class="{'active':$last}">
<span>
<a ng-href="{$ oc.containerURL + oc.model.pseudo_folder_hierarchy.slice(0, $index + 1).join(oc.model.DELIMETER) $}"
ng-if="!$last">{$ pf $}</a>
<span ng-if="$last">{$ pf $}</span>
</span>
</li>
</ol>
</th>
</tr>
<tr class="table_caption">
<th colspan="4" class="search-header">
<hz-search-bar group-classes="input-group-sm"
icon-classes="fa-search" input-classes="form-control" placeholder="Filter">
</hz-search-bar>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="file in displayContents track by $index">
<td>
<a ng-if="file.is_subdir" ng-href="{$ oc.currentURL + file.name $}">{$ file.name $}</a>
<span ng-if="file.is_object">{$ file.name $}</span>
</td>
<td>
<span ng-if="file.is_object">{$file.bytes | bytes$}</span>
<span ng-if="file.is_subdir"><translate>folder</translate></span>
</td>
<td>
</td>
</tr>
<tr hz-no-items items="displayContents">
</tr>
</tbody>
<tfoot hz-table-footer items="displayContents"></tfoot>
</table>

View File

@ -0,0 +1,7 @@
<table class="table table-bordered tablestriped">
<tbody>
<tr class="odd empty">
<td><translate>Select a container to browse.</translate></td>
</tr>
</tbody>
</table>

View File

@ -25,6 +25,7 @@
*/
angular
.module('horizon.dashboard.project', [
'horizon.dashboard.project.containers',
'horizon.dashboard.project.images',
'horizon.dashboard.project.workflow'
])

View File

@ -24,3 +24,7 @@ ADD_PANEL = ('openstack_dashboard.dashboards.project.'
'ngcontainers.panel.NGContainers')
DISABLED = True
ADD_SCSS_FILES = [
'dashboard/project/containers/_containers.scss',
]

View File

@ -96,7 +96,8 @@
'horizon.framework.util.tech-debt.helper-functions',
'$cookieStore',
'$http',
'$cookies'
'$cookies',
'$route'
];
function updateHorizon(
@ -105,7 +106,8 @@
hzUtils,
$cookieStore,
$http,
$cookies
$cookies,
$route
) {
$http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
@ -124,6 +126,11 @@
gettextCatalog.setCurrentLanguage(horizon.languageCode);
gettextCatalog.setStrings(horizon.languageCode, django.catalog);
// because of angular startup, and our use of ng-include with
// embedded ng-view, we need to re-kick ngRoute after everything's
// resolved
$route.reload();
/*
* cookies are updated at the end of current $eval, so for the horizon
* namespace we need to wrap it in a $apply function.

View File

@ -4,5 +4,4 @@
@import "components/forms";
@import "components/navbar";
@import "components/navs";
@import "components/panels";
@import "components/type";

View File

@ -2,6 +2,7 @@
@import "components/breadcrumb_header";
@import "components/context_selection";
@import "components/login";
@import "components/messages";
@import "components/navbar";
@import "components/pie_charts";

View File

@ -1,3 +1,3 @@
.panel {
.login .panel {
@include box-shadow(0 3px 7px rgba(0, 0, 0, 0.3));
}