diff --git a/horizon/static/framework/util/filters/filters.js b/horizon/static/framework/util/filters/filters.js index f6c8a728fc..3efcc36fdb 100644 --- a/horizon/static/framework/util/filters/filters.js +++ b/horizon/static/framework/util/filters/filters.js @@ -19,6 +19,7 @@ angular .module('horizon.framework.util.filters') .filter('yesno', yesNoFilter) + .filter('simpleDate', simpleDateFilter) .filter('gb', gbFilter) .filter('mb', mbFilter) .filter('title', titleFilter) @@ -45,6 +46,19 @@ }; } + /** + * @ngdoc filter + * @name simpleDate + * @description + * Evaluates given for display as a short date, returning '-' if empty. + */ + simpleDateFilter.$inject = ['$filter']; + function simpleDateFilter($filter) { + return function (input) { + return $filter('noValue')($filter('date')(input, 'short')); + }; + } + /** * @ngdoc filter * @name gb diff --git a/horizon/static/framework/util/filters/filters.spec.js b/horizon/static/framework/util/filters/filters.spec.js index 789c59a76e..2ed211fcd8 100644 --- a/horizon/static/framework/util/filters/filters.spec.js +++ b/horizon/static/framework/util/filters/filters.spec.js @@ -38,6 +38,21 @@ }); }); + describe('simpleDate', function () { + var simpleDateFilter; + beforeEach(inject(function (_simpleDateFilter_) { + simpleDateFilter = _simpleDateFilter_; + })); + + it('returns blank if nothing', function () { + expect(simpleDateFilter()).toBe('-'); + }); + + it('returns the expected time', function() { + expect(simpleDateFilter('2016-06-24T04:19:07')).toBe('6/24/16 4:19 AM'); + }); + }); + describe('gb', function () { var gbFilter; beforeEach(inject(function (_gbFilter_) { diff --git a/openstack_dashboard/static/app/core/images/admin-panel.html b/openstack_dashboard/static/app/core/images/admin-panel.html new file mode 100644 index 0000000000..444e4ec919 --- /dev/null +++ b/openstack_dashboard/static/app/core/images/admin-panel.html @@ -0,0 +1,5 @@ + + + diff --git a/openstack_dashboard/static/app/core/images/details/details.module.js b/openstack_dashboard/static/app/core/images/details/details.module.js index 8bf853221c..299a98bdbe 100644 --- a/openstack_dashboard/static/app/core/images/details/details.module.js +++ b/openstack_dashboard/static/app/core/images/details/details.module.js @@ -30,27 +30,23 @@ registerImageDetails.$inject = [ 'horizon.app.core.images.basePath', 'horizon.app.core.images.resourceType', - 'horizon.app.core.openstack-service-api.glance', + 'horizon.app.core.images.service', 'horizon.framework.conf.resource-type-registry.service' ]; function registerImageDetails( basePath, imageResourceType, - glanceApi, + imageService, registry ) { registry.getResourceType(imageResourceType) - .setLoadFunction(loadFunction) + .setLoadFunction(imageService.getImagePromise) .detailsViews.append({ id: 'imageDetailsOverview', name: gettext('Overview'), template: basePath + 'details/overview.html' }); - - function loadFunction(identifier) { - return glanceApi.getImage(identifier); - } } })(); diff --git a/openstack_dashboard/static/app/core/images/details/drawer.html b/openstack_dashboard/static/app/core/images/details/drawer.html index 1d2d4a856f..2432938c0c 100644 --- a/openstack_dashboard/static/app/core/images/details/drawer.html +++ b/openstack_dashboard/static/app/core/images/details/drawer.html @@ -5,7 +5,7 @@ item="item" property-groups="[ ['name', 'id'], - ['is_public', 'protected'], + ['visibility', 'protected'], ['disk_format', 'size'], ['min_disk', 'min_ram']]"> diff --git a/openstack_dashboard/static/app/core/images/details/overview.controller.js b/openstack_dashboard/static/app/core/images/details/overview.controller.js index df8a5a45c8..73691f033a 100644 --- a/openstack_dashboard/static/app/core/images/details/overview.controller.js +++ b/openstack_dashboard/static/app/core/images/details/overview.controller.js @@ -23,12 +23,14 @@ ImageOverviewController.$inject = [ 'horizon.app.core.images.resourceType', 'horizon.framework.conf.resource-type-registry.service', + 'horizon.app.core.openstack-service-api.userSession', '$scope' ]; function ImageOverviewController( imageResourceTypeCode, registry, + userSession, $scope ) { var ctrl = this; @@ -44,6 +46,12 @@ ctrl.image.properties = Object.keys(ctrl.image.properties).map(function mapProps(prop) { return {name: prop, value: ctrl.image.properties[prop]}; }); + + userSession.get().then(setProject); + + function setProject(session) { + ctrl.projectId = session.project_id; + } } } diff --git a/openstack_dashboard/static/app/core/images/details/overview.controller.spec.js b/openstack_dashboard/static/app/core/images/details/overview.controller.spec.js index 5639b30ee5..e1672e80a4 100644 --- a/openstack_dashboard/static/app/core/images/details/overview.controller.spec.js +++ b/openstack_dashboard/static/app/core/images/details/overview.controller.spec.js @@ -25,10 +25,14 @@ beforeEach(module('horizon.app.core.images')); beforeEach(module('horizon.framework.conf')); - beforeEach(inject(function($controller, $q) { + beforeEach(inject(function($controller, $q, $injector) { + var session = $injector.get('horizon.app.core.openstack-service-api.userSession'); var deferred = $q.defer(); + var sessionDeferred = $q.defer(); deferred.resolve({data: {properties: {'a': 'apple'}}}); + deferred.resolve({project_id: '12'}); spyOn(glance, 'getNamespaces').and.returnValue(deferred.promise); + spyOn(session, 'get').and.returnValue(sessionDeferred.promise); ctrl = $controller('ImageOverviewController', { '$scope': {context: {loadPromise: deferred.promise}} diff --git a/openstack_dashboard/static/app/core/images/details/overview.html b/openstack_dashboard/static/app/core/images/details/overview.html index 6bd05a9870..d4eb5c63a2 100644 --- a/openstack_dashboard/static/app/core/images/details/overview.html +++ b/openstack_dashboard/static/app/core/images/details/overview.html @@ -8,45 +8,35 @@ cls="dl-horizontal" item="ctrl.image" property-groups="[[ - 'type', 'status', 'size', 'min_disk', 'min_ram', 'disk_format', - 'container_format']]"> + 'id', 'type', 'status', 'size', 'min_disk', 'min_ram', 'disk_format', + 'container_format', 'created_at', 'updated_at']]">

{$ 'Security' | translate $}


+
Owner
+
{$ ctrl.image.owner $}
Filename
-
{$ ctrl.image.properties.filename $}
+
{$ ctrl.image.properties.filename | noValue $}
Visibility
-
{$ ctrl.image | imageVisibility $}
+
{$ ctrl.image | imageVisibility:ctrl.projectId $}
Protected
{$ ctrl.image.protected | yesno $}
Checksum
-
{$ ctrl.image.checksum $}
+
{$ ctrl.image.checksum | noValue $}
-
-

Record Properties

-
-
-
Created
-
{$ ctrl.image.created_at | date:'short' $}
-
Updated
-
{$ ctrl.image.updated_at | date:'short' $}
-
ID
-
{$ ctrl.image.id $}
-
-

Custom Properties


{$ ctrl.resourceType.label(prop.name) $}
-
{$ ctrl.resourceType.format(prop.name, prop.value) $}
+
{$ prop.value $}
diff --git a/openstack_dashboard/static/app/core/images/images.module.js b/openstack_dashboard/static/app/core/images/images.module.js index c0a7e6acee..14a4e21e81 100644 --- a/openstack_dashboard/static/app/core/images/images.module.js +++ b/openstack_dashboard/static/app/core/images/images.module.js @@ -81,7 +81,8 @@ filters: ['uppercase'] }) .setProperty('created_at', { - label: gettext('Created At') + label: gettext('Created At'), + filters: ['simpleDate'] }) .setProperty('disk_format', { label: gettext('Disk Format'), @@ -90,10 +91,6 @@ .setProperty('id', { label: gettext('ID') }) - .setProperty('is_public', { - label: gettext('Is Public'), - filters: ['yesno'] - }) .setProperty('type', { label: gettext('Type'), filters: [imagesService.imageType] @@ -129,7 +126,8 @@ label: gettext('Tags') }) .setProperty('updated_at', { - label: gettext('Updated At') + label: gettext('Updated At'), + filters: ['simpleDate'] }) .setProperty('virtual_size', { label: gettext('Virtual Size') @@ -172,6 +170,10 @@ priority: 1, itemInTransitionFunction: imagesService.isInTransition }) + .append({ + id: 'visibility', + priority: 1 + }) .append({ id: 'protected', priority: 1 @@ -208,6 +210,18 @@ {label: gettext('Deleted'), key: 'deleted'} ] }) + .append({ + label: gettext('Visibility'), + name: 'visibility', + isServer: false, + singleton: true, + options: [ + {label: gettext('Public'), key: gettext('Public')}, + {label: gettext('Private'), key: gettext('Private')}, + {label: gettext('Shared With Project'), key: gettext('Shared With Project')}, + {label: gettext('Unknown'), key: 'unknown'} + ] + }) .append({ label: gettext('Protected'), name: 'protected', @@ -330,7 +344,7 @@ }); $routeProvider.when('/admin/images/', { - templateUrl: path + 'panel.html' + templateUrl: path + 'admin-panel.html' }); function goToAngularDetails(params) { diff --git a/openstack_dashboard/static/app/core/images/images.service.js b/openstack_dashboard/static/app/core/images/images.service.js index ef4afe854f..23b0c9b59c 100644 --- a/openstack_dashboard/static/app/core/images/images.service.js +++ b/openstack_dashboard/static/app/core/images/images.service.js @@ -20,7 +20,9 @@ .factory('horizon.app.core.images.service', imageService); imageService.$inject = [ + '$filter', 'horizon.app.core.openstack-service-api.glance', + 'horizon.app.core.openstack-service-api.userSession', 'horizon.app.core.images.transitional-statuses' ]; @@ -34,9 +36,10 @@ * but do not need to be restricted to such use. Each exposed function * is documented below. */ - function imageService(glance, transitionalStatuses) { + function imageService($filter, glance, userSession, transitionalStatuses) { return { getDetailsPath: getDetailsPath, + getImagePromise: getImagePromise, getImagesPromise: getImagesPromise, imageType: imageType, isInTransition: isInTransition @@ -99,17 +102,34 @@ * 'trackBy' to assist the display mechanism when updating rows. */ function getImagesPromise(params) { - return glance.getImages(params).then(modifyResponse); + var projectId; + return userSession.get().then(getImages); + + function getImages(userSession) { + projectId = userSession.project_id; + return glance.getImages(params).then(modifyResponse); + } function modifyResponse(response) { - return {data: {items: response.data.items.map(addTrackBy)}}; + return {data: {items: response.data.items.map(modifyImage)}}; - function addTrackBy(image) { + function modifyImage(image) { image.trackBy = image.id + image.updated_at; + image.visibility = $filter('imageVisibility')(image, projectId); return image; } } } + + /* + * @ngdoc function + * @name getImagePromise + * @description + * Given an id, returns a promise for the image data. + */ + function getImagePromise(identifier) { + return glance.getImage(identifier); + } } })(); diff --git a/openstack_dashboard/static/app/core/images/images.service.spec.js b/openstack_dashboard/static/app/core/images/images.service.spec.js index f8ac6db19b..d298b022b3 100644 --- a/openstack_dashboard/static/app/core/images/images.service.spec.js +++ b/openstack_dashboard/static/app/core/images/images.service.spec.js @@ -86,14 +86,30 @@ describe('getImagesPromise', function() { it("provides a promise that gets translated", inject(function($q, $injector, $timeout) { var glance = $injector.get('horizon.app.core.openstack-service-api.glance'); + var session = $injector.get('horizon.app.core.openstack-service-api.userSession'); var deferred = $q.defer(); + var deferredSession = $q.defer(); spyOn(glance, 'getImages').and.returnValue(deferred.promise); + spyOn(session, 'get').and.returnValue(deferredSession.promise); var result = service.getImagesPromise({}); deferred.resolve({data: {items: [{id: 1, updated_at: 'jul1'}]}}); + deferredSession.resolve({project_id: '12'}); $timeout.flush(); expect(result.$$state.value.data.items[0].trackBy).toBe('1jul1'); })); }); + + describe('getImagePromise', function() { + it("provides a promise", inject(function($q, $injector) { + var glance = $injector.get('horizon.app.core.openstack-service-api.glance'); + var deferred = $q.defer(); + spyOn(glance, 'getImage').and.returnValue(deferred.promise); + var result = service.getImagePromise({}); + deferred.resolve({id: 1, updated_at: 'jul1'}); + expect(glance.getImage).toHaveBeenCalled(); + expect(result.$$state.value.updated_at).toBe('jul1'); + })); + }); }); })(); diff --git a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.js b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.js index 440901c58b..c140d7ca00 100644 --- a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.js +++ b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.js @@ -155,7 +155,14 @@ } function onGetAvailabilityZones(response) { - ctrl.availabilityZones = response.items; + ctrl.availabilityZones = response.items.map(justNames); + if (ctrl.availabilityZones.length > 0) { + ctrl.volume.availability_zone = ctrl.availabilityZones[0]; + } + + function justNames(item) { + return item.zoneName; + } } function onGetAbsoluteLimits(response) { diff --git a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.spec.js b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.spec.js index 8ac3f2cd42..3f0abd9dc9 100644 --- a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.spec.js +++ b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.controller.spec.js @@ -17,7 +17,7 @@ describe('horizon.app.core.images.controller.CreateVolumeController', function () { - var controller, quotaChartDefaults, $scope, $filter, getAbsoluteLimitsSpy; + var controller, quotaChartDefaults, $scope, $filter, getAbsoluteLimitsSpy, nova; var cinder = { getVolumeTypes: function() { @@ -39,21 +39,21 @@ } }; - var nova = { - getAvailabilityZones: function() { - return { - success: function(callback) { - return callback({ items: ['zone1'] }); - } - }; - } - }; - beforeEach(module('horizon.app.core.images')); beforeEach(module('horizon.framework.widgets.charts')); beforeEach(module('horizon.framework.util.filters')); beforeEach(inject(function ($injector, _$rootScope_, _$filter_) { + + nova = { + getAvailabilityZones: function() { + return { + success: function(callback) { + return callback({ items: [{zoneName: 'zone1'}] }); + } + }; + } + }; $scope = _$rootScope_.$new(); $scope.image = { name: 'ImageName', @@ -364,6 +364,37 @@ ctrl.volume.size = 100; + var emittedEventArgs = $scope.$emit.calls.argsFor(0); + var expectedVolume = { + size: 100, + name: ctrl.image.name, + description: '', + volume_type: 'lvmdriver-1', + availability_zone: 'zone1', // pre-selects first + metadata: {}, + image_id: ctrl.image.id, + snapshot_id: null, + source_volid: null + }; + + expect(emittedEventArgs[0]).toEqual('horizon.app.core.images.VOLUME_CHANGED'); + expect(emittedEventArgs[1]).toEqual(expectedVolume); + }); + + it('not default the availability_zone if none present', function() { + + nova.getAvailabilityZones = function() { + return { + success: function(callback) { + return callback({ items: [] }); + } + }; + }; + var ctrl = createController(); + $scope.$apply(); + + ctrl.volume.size = 100; + var emittedEventArgs = $scope.$emit.calls.argsFor(0); var expectedVolume = { size: 100, diff --git a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.help.html b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.help.html new file mode 100644 index 0000000000..0f23c7f741 --- /dev/null +++ b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.help.html @@ -0,0 +1,4 @@ +
+

Description

+

This page allows you to create a volume based off of the image.

+
diff --git a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.html b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.html index 78562210eb..0b94aab18d 100644 --- a/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.html +++ b/openstack_dashboard/static/app/core/images/steps/create-volume/create-volume.html @@ -49,12 +49,11 @@ Availability Zone -
diff --git a/openstack_dashboard/static/app/core/images/workflows/create-volume.service.js b/openstack_dashboard/static/app/core/images/workflows/create-volume.service.js index 6c3e055433..58f838ff08 100644 --- a/openstack_dashboard/static/app/core/images/workflows/create-volume.service.js +++ b/openstack_dashboard/static/app/core/images/workflows/create-volume.service.js @@ -43,6 +43,7 @@ { title: gettext('Volume Details'), templateUrl: basePath + 'steps/create-volume/create-volume.html', + helpUrl: basePath + 'steps/create-volume/create-volume.help.html', formName: 'volumeForm' } ]