diff --git a/horizon/static/framework/widgets/action-list/action.directive.js b/horizon/static/framework/widgets/action-list/action.directive.js
index 2e62c74059..1a980a06ca 100644
--- a/horizon/static/framework/widgets/action-list/action.directive.js
+++ b/horizon/static/framework/widgets/action-list/action.directive.js
@@ -37,7 +37,7 @@
* Attributes:
*
* actionClasses: classes added to button or link
- * callback: function called when button or link clicked
+ * callback: function called when button clicked or link needed for rendering
* disabled: disable/enable button dynamically
* item: object passed to callback function
*
@@ -57,6 +57,10 @@
*
* Delete
*
+ *
+ *
+ * Download
+ *
* ```
*/
angular
diff --git a/horizon/static/framework/widgets/action-list/actions-link.template.html b/horizon/static/framework/widgets/action-list/actions-link.template.html
new file mode 100644
index 0000000000..cdf5635714
--- /dev/null
+++ b/horizon/static/framework/widgets/action-list/actions-link.template.html
@@ -0,0 +1,3 @@
+
+ $text$
+
diff --git a/horizon/static/framework/widgets/action-list/actions.directive.js b/horizon/static/framework/widgets/action-list/actions.directive.js
index 35fe62518f..a8ceddad74 100644
--- a/horizon/static/framework/widgets/action-list/actions.directive.js
+++ b/horizon/static/framework/widgets/action-list/actions.directive.js
@@ -81,6 +81,7 @@
* 2. 'danger' - For marking an Action as dangerous. Only for 'row' type.
* 3. 'delete-selected' - Delete multiple rows. Only for 'batch' type.
* 4. 'create' - Create a new entity. Only for 'batch' type.
+ * 5. 'link' - Generates a link instead of button. Only for 'row' type.
*
* The styling of the action button is done based on the 'listType'.
* The directive will be responsible for binding the correct callback.
@@ -101,6 +102,8 @@
* When using 'row' type, the current 'item' is evaluated and passed to the function.
* When using 'batch' type, 'item' is not passed.
* When using 'delete-selected' for 'batch' type, all selected rows are passed.
+ * When using 'link' this is invoked during rendering with the current 'item' passed
+ * and should return the URL for the link.
*
* @restrict E
* @scope
@@ -186,6 +189,15 @@
* }
* };
*
+ * var downloadService = {
+ * allowed: function(image) {
+ * return isPublic(image);
+ * },
+ * perform: function(image) {
+ * return generateUrlFor(image);
+ * }
+ * };
+ *
* Then create the Service to use in the HTML which lists
* all allowed actions with the templates to use.
*
@@ -201,6 +213,12 @@
* text: gettext('Create Volume')
* },
* service: createVolumeService
+ * }, {
+ * template: {
+ * text: gettext('Download'),
+ * type: 'link',
+ * },
+ * service: downloadService
* }];
* }
*
diff --git a/horizon/static/framework/widgets/action-list/actions.service.js b/horizon/static/framework/widgets/action-list/actions.service.js
index 3888624281..dd5c8f69ec 100644
--- a/horizon/static/framework/widgets/action-list/actions.service.js
+++ b/horizon/static/framework/widgets/action-list/actions.service.js
@@ -153,7 +153,11 @@
*/
function getSplitButton(actionTemplate) {
var actionElement = angular.element(actionTemplate.template);
- actionElement.attr('button-type', 'split-button');
+ var type = actionTemplate.type;
+ if (type !== 'link') {
+ type = 'button';
+ }
+ actionElement.attr('button-type', 'split-' + type);
actionElement.attr('action-classes', actionElement.attr('action-classes'));
actionElement.attr('callback', actionTemplate.callback);
return actionElement;
@@ -184,8 +188,8 @@
function getTemplate(permittedAction, index, permittedActions) {
var defered = $q.defer();
var action = permittedAction.context;
- $http.get(getTemplateUrl(action, permittedActions.length), {cache: $templateCache})
- .then(onTemplateGet);
+ var url = getTemplateUrl(action, permittedActions.length);
+ $http.get(url, {cache: $templateCache}).then(onTemplateGet);
return defered.promise;
function onTemplateGet(response) {
@@ -198,6 +202,7 @@
.replace('$item$', item);
defered.resolve({
template: template,
+ type: action.template.type || 'button',
callback: callback
});
}
diff --git a/horizon/static/framework/widgets/action-list/link.html b/horizon/static/framework/widgets/action-list/link.html
new file mode 100644
index 0000000000..ae392466f9
--- /dev/null
+++ b/horizon/static/framework/widgets/action-list/link.html
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/horizon/static/framework/widgets/action-list/menu-item.html b/horizon/static/framework/widgets/action-list/menu-item.html
index 086a1efdbc..b6c8aa4401 100644
--- a/horizon/static/framework/widgets/action-list/menu-item.html
+++ b/horizon/static/framework/widgets/action-list/menu-item.html
@@ -1,8 +1,9 @@
+
+ Note: Delimiters ('{$ ctrl.model.DELIMETER $}') are allowed in the
+ folder name to create deep folders.
+
+
+
+
+
+
+
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/file-change-directive.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/file-change-directive.js
new file mode 100644
index 0000000000..67318716a8
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/file-change-directive.js
@@ -0,0 +1,44 @@
+/*
+ * (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';
+
+ angular
+ .module('horizon.dashboard.project.containers')
+ .directive('onFileChange', OnFileChange);
+
+ OnFileChange.$inject = [];
+
+ function OnFileChange() {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function link(scope, element, attrs, ngModel) {
+ var onFileChangeHandler = scope.$eval(attrs.onFileChange);
+ element.on('change', function change(event) {
+ onFileChangeHandler(event.target.files);
+ // we need to manually change the view element and force a render
+ // to have angular pick up that the file upload now has a value
+ // and any required constraint is now satisfied
+ scope.$apply(function expression() {
+ ngModel.$setViewValue(event.target.files[0].name);
+ ngModel.$render();
+ });
+ });
+ }
+ };
+ }
+})();
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/file-change-directive.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/file-change-directive.spec.js
new file mode 100644
index 0000000000..6349a14c80
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/file-change-directive.spec.js
@@ -0,0 +1,54 @@
+/*
+ * (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.dashboard.project.containers'));
+
+ var $compile, $scope;
+
+ beforeEach(inject(function inject($injector, _$rootScope_) {
+ $scope = _$rootScope_.$new();
+ $compile = $injector.get('$compile');
+ }));
+
+ it('should detect changes to file selection and update things', function test() {
+ // set up scope for the elements below
+ $scope.model = '';
+ $scope.changed = angular.noop;
+ spyOn($scope, 'changed');
+
+ var element = angular.element(
+ '
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-controller.js
new file mode 100644
index 0000000000..30be321012
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-controller.js
@@ -0,0 +1,54 @@
+/*
+ * (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';
+
+ angular
+ .module('horizon.dashboard.project.containers')
+ .controller('UploadObjectModalController', UploadObjectModalController);
+
+ UploadObjectModalController.$inject = [
+ 'horizon.dashboard.project.containers.containers-model',
+ '$scope'
+ ];
+
+ function UploadObjectModalController(model, $scope) {
+ var ctrl = this;
+
+ ctrl.model = {
+ name:'',
+ container: model.container,
+ folder: model.folder,
+ view_file: null, // file object managed by angular form ngModel
+ upload_file: null, // file object from the DOM element with the actual upload
+ DELIMETER: model.DELIMETER
+ };
+ ctrl.changeFile = changeFile;
+
+ ///////////
+
+ function changeFile(files) {
+ if (files.length) {
+ // update the upload file & its name
+ ctrl.model.upload_file = files[0];
+ ctrl.model.name = files[0].name;
+
+ // we're modifying the model value from a DOM event so we need to manually $digest
+ $scope.$digest();
+ }
+ }
+ }
+})();
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-controller.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-controller.spec.js
new file mode 100644
index 0000000000..27b4940ee7
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-controller.spec.js
@@ -0,0 +1,71 @@
+/**
+ * (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 upload-object controller', function() {
+ var controller, $scope;
+
+ beforeEach(module('horizon.framework'));
+ beforeEach(module('horizon.dashboard.project.containers'));
+
+ beforeEach(module(function ($provide) {
+ $provide.value('horizon.dashboard.project.containers.containers-model', {
+ container: {name: 'spam'},
+ folder: 'ham'
+ });
+ }));
+
+ beforeEach(inject(function ($injector, _$rootScope_) {
+ controller = $injector.get('$controller');
+ $scope = _$rootScope_.$new(true);
+ }));
+
+ function createController() {
+ return controller('UploadObjectModalController', {$scope: $scope});
+ }
+
+ it('should initialise the controller model when created', function test() {
+ var ctrl = createController();
+ expect(ctrl.model.name).toEqual('');
+ expect(ctrl.model.container.name).toEqual('spam');
+ expect(ctrl.model.folder).toEqual('ham');
+ });
+
+ it('should respond to file changes correctly', function test() {
+ var ctrl = createController();
+ spyOn($scope, '$digest');
+ var file = {name: 'eggs'};
+
+ ctrl.changeFile([file]);
+
+ expect(ctrl.model.name).toEqual('eggs');
+ expect(ctrl.model.upload_file).toEqual(file);
+ expect($scope.$digest).toHaveBeenCalled();
+ });
+
+ it('should respond to file changes correctly if no files are present', function test() {
+ var ctrl = createController();
+ spyOn($scope, '$digest');
+
+ ctrl.changeFile([]);
+
+ expect(ctrl.model.name).toEqual('');
+ expect($scope.$digest).not.toHaveBeenCalled();
+ });
+ });
+})();
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-modal.html b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-modal.html
new file mode 100644
index 0000000000..3a837d520f
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/upload-object-modal.html
@@ -0,0 +1,53 @@
+
+ Note: Delimiters ('{$ ctrl.model.DELIMETER $}') are allowed in the
+ file name to place the new file into a folder that will be created
+ when the file is uploaded (to any depth of folders).
+
+
+
+
+
+
+
+
diff --git a/openstack_dashboard/static/dashboard/scss/_debt.scss b/openstack_dashboard/static/dashboard/scss/_debt.scss
index 7f682fd29b..2a894b66f1 100644
--- a/openstack_dashboard/static/dashboard/scss/_debt.scss
+++ b/openstack_dashboard/static/dashboard/scss/_debt.scss
@@ -262,3 +262,14 @@ td .btn-group {
z-index: 12000;
word-wrap: break-word;
}
+
+/*
+Hack to allow a
to be wrapped around a disabled element that
+needs to have a tooltip. The disabled element won't allow a JS tooltip
+to receive events, so we wrap it in another tag. For some reason a
+ also doesn't receive the events, but a
does. We set
+display to inline-block so that existing formatting is unaffected.
+*/
+div.tooltip-hack {
+ display: inline-block;
+}
diff --git a/openstack_dashboard/themes/material/static/horizon/_icons.scss b/openstack_dashboard/themes/material/static/horizon/_icons.scss
index cf5e3adc59..aeaa6d3cee 100644
--- a/openstack_dashboard/themes/material/static/horizon/_icons.scss
+++ b/openstack_dashboard/themes/material/static/horizon/_icons.scss
@@ -2,6 +2,7 @@
// This file does a 1-1 mapping of each font-awesome icon in use to
// a corresponding Material Design Icon.
+// https://materialdesignicons.com
$mdi-font-path: $static_url + "/horizon/lib/mdi/fonts";
@import "/horizon/lib/mdi/scss/materialdesignicons.scss";
@@ -35,6 +36,8 @@ $icon-swap: (
exclamation-triangle: 'alert',
eye: 'eye',
eye-slash: 'eye-off',
+ folder: 'folder',
+ folder-o: 'folder-outline',
group: 'account-multiple',
home: 'home',
link: 'link-variant',
diff --git a/releasenotes/notes/bg-angularize-swift-9a1b44aa3646bc8c.yaml b/releasenotes/notes/bg-angularize-swift-9a1b44aa3646bc8c.yaml
new file mode 100644
index 0000000000..dae8373ee9
--- /dev/null
+++ b/releasenotes/notes/bg-angularize-swift-9a1b44aa3646bc8c.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Move OpenStack Dashboard Swift panel rendering logic to client-side
+ using AngularJS for significant usability improvements. Set ``DISABLED=False`` in
+ ``enabled/_1921_project_ng_containers_panel.py`` to enable.