Image Detail Redesign (Angular/UX)
Image Detail UX redesign, implemented in AngularJS. This uses the design at http://invis.io/962Q35HVQ as its base. For now, basic properties are displayed. The actions on the detail screen will be implemented in a later patch. To test, modify openstack_dashboard/enabled/_1051_project_ng_images_panel.py and set DISABLED = False There's a bug around translation of images which is introduced by this change https://bugs.launchpad.net/horizon/+bug/1487590 Partially-Implements: blueprint angularize-images-table Co-Authored-By: Rajat Vig <rajatv@thoughtworks.com> Co-Authored-By: Dan Siwiec <dan.siwiec@thoughtworks.com> Co-Authored-By: Kyle Olivo <kyle@kyleolivo.com> Co-Authored-By: Coleman Beasley <coleman.beasley@thoughtworks.com> Co-Authored-By: Matt Borland <matt.borland@hpe.com> Co-Authored-By: Tyr Johanson <tyr@hpe.com> Change-Id: I9882970b40b52a402e5693f6993f1b50c0a819f6
This commit is contained in:
parent
80bbc35944
commit
1a17b8608f
@ -1,11 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Images" %}{% endblock %}
|
||||
{% block page_header %}{% endblock %}
|
||||
|
||||
{% block ng_route_base %}
|
||||
<base href="{{ WEBROOT }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<ng-include src="'{{ STATIC_URL }}app/core/images/table/images-table.html'"></ng-include>
|
||||
<div ng-view></div>
|
||||
{% endblock %}
|
||||
|
@ -20,5 +20,5 @@ from openstack_dashboard.dashboards.project.ngimages import views
|
||||
|
||||
urlpatterns = patterns(
|
||||
'openstack_dashboard.dashboards.project.ngimages.views',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url('', views.IndexView.as_view(), name='index'),
|
||||
)
|
||||
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
*
|
||||
* 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.app.core.images')
|
||||
.controller('ImageDetailController', ImageDetailController);
|
||||
|
||||
ImageDetailController.$inject = [
|
||||
'horizon.app.core.images.tableRoute',
|
||||
'horizon.app.core.openstack-service-api.glance',
|
||||
'horizon.app.core.openstack-service-api.keystone',
|
||||
'$routeParams'
|
||||
];
|
||||
|
||||
function ImageDetailController(
|
||||
tableRoute,
|
||||
glanceAPI,
|
||||
keystoneAPI,
|
||||
$routeParams)
|
||||
{
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.image = {};
|
||||
ctrl.project = {};
|
||||
ctrl.hasCustomProperties = false;
|
||||
ctrl.tableRoute = tableRoute;
|
||||
|
||||
var imageId = $routeParams.imageId;
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
// Load the elements that are used in the overview.
|
||||
glanceAPI.getImage(imageId).success(onGetImage);
|
||||
|
||||
ctrl.hasCustomProperties =
|
||||
angular.isDefined(ctrl.image) &&
|
||||
angular.isDefined(ctrl.image.properties);
|
||||
}
|
||||
|
||||
function onGetImage(image) {
|
||||
ctrl.image = image;
|
||||
|
||||
ctrl.image.properties = Object.keys(ctrl.image.properties).map(function mapProps(prop) {
|
||||
return {name: prop, value: ctrl.image.properties[prop]};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
*
|
||||
* 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.app.core.images', function() {
|
||||
|
||||
beforeEach(module('ui.bootstrap'));
|
||||
beforeEach(module('horizon.app.core'));
|
||||
|
||||
describe("ImageDetailController", function() {
|
||||
var ctrl, glanceAPI, keystoneAPI, imageMock, projectMock, tableRoute;
|
||||
|
||||
beforeEach(inject(function($injector, $controller) {
|
||||
imageMock = {
|
||||
owner: 'mock_image_owner',
|
||||
properties: {
|
||||
kernel_id: 'mock_kernel_id'
|
||||
}
|
||||
};
|
||||
|
||||
projectMock = {
|
||||
name: 'mock_project'
|
||||
};
|
||||
|
||||
keystoneAPI = {
|
||||
getProject: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback(projectMock);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
glanceAPI = {
|
||||
getImage: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback(imageMock);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(glanceAPI, 'getImage').and.callThrough();
|
||||
spyOn(keystoneAPI, 'getProject').and.callThrough();
|
||||
|
||||
tableRoute = $injector.get('horizon.app.core.images.tableRoute');
|
||||
|
||||
ctrl = $controller("ImageDetailController", {
|
||||
'horizon.app.core.openstack-service-api.glance': glanceAPI,
|
||||
'horizon.app.core.openstack-service-api.keystone': keystoneAPI,
|
||||
'$routeParams': {
|
||||
imageId: '1234'
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
it('defines the controller', function() {
|
||||
expect(ctrl).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set table route', function() {
|
||||
expect(ctrl.tableRoute).toEqual(tableRoute);
|
||||
});
|
||||
|
||||
it('should create a map of the image properties', function() {
|
||||
expect(ctrl.hasCustomProperties).toEqual(true);
|
||||
expect(ctrl.image.properties).toEqual([{name: 'kernel_id', value: 'mock_kernel_id'}]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,106 @@
|
||||
<div ng-controller="ImageDetailController as ctrl">
|
||||
<div class="page-header">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{$ ctrl.tableRoute $}"><translate>Images</translate></a></li>
|
||||
<li class="active">{$ ::ctrl.image.name $}</li>
|
||||
</ol>
|
||||
<p>{$ ctrl.image.properties.description $}</p>
|
||||
<ul class="list-inline">
|
||||
<li>
|
||||
<strong translate>Status</strong>
|
||||
{$ ::ctrl.image.status $}
|
||||
</li>
|
||||
<li ng-if="ctrl.image.properties.filename">
|
||||
<strong translate>Filename</strong>
|
||||
{$ ::ctrl.image.properties.filename $}
|
||||
</li>
|
||||
<li>
|
||||
<strong translate>Type</strong>
|
||||
{$ ctrl.image | imageType $}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<tabset>
|
||||
<tab heading="{$ 'Overview' | translate $}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 detail">
|
||||
<h3 translate>Image</h3>
|
||||
<hr>
|
||||
<dl class="dl-horizontal">
|
||||
<div>
|
||||
<dt translate>Size</dt>
|
||||
<dd>{$ ctrl.image.size | bytes $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Min. Disk</dt>
|
||||
<dd>{$ ctrl.image.min_disk | gb $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Min. RAM</dt>
|
||||
<dd>{$ ctrl.image.min_ram | mb $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Disk Format</dt>
|
||||
<dd>{$ ctrl.image.disk_format | uppercase $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Container Format</dt>
|
||||
<dd>{$ ctrl.image.container_format | uppercase $}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6 detail">
|
||||
<h3>{$ 'Security' | translate $}</h3>
|
||||
<hr>
|
||||
<dl class="dl-horizontal">
|
||||
<div>
|
||||
<dt translate>Visibility</dt>
|
||||
<dd>{$ ctrl.image | imageVisibility $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Protected</dt>
|
||||
<dd>{$ ::ctrl.image.protected | yesno $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Checksum</dt>
|
||||
<dd>{$ ::ctrl.image.checksum $}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 detail">
|
||||
<h3 translate>Record Properties</h3>
|
||||
<hr>
|
||||
<dl class="dl-horizontal">
|
||||
<div>
|
||||
<dt translate>Created</dt>
|
||||
<dd>{$ ctrl.image.created_at | date:'short' $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>Updated</dt>
|
||||
<dd>{$ ctrl.image.updated_at | date:'short' $}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt translate>ID</dt>
|
||||
<dd>{$ ctrl.image.id $}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6 detail">
|
||||
<h3 translate>Custom Properties</h3>
|
||||
<hr>
|
||||
<dl class="dl-horizontal">
|
||||
<div ng-repeat="prop in ctrl.image.properties">
|
||||
<div>
|
||||
<dt data-toggle="tooltip" title="{$ prop.name $}">{$ prop.name $}</dt>
|
||||
<dd>{$ prop.value $}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</tab>
|
||||
</tabset>
|
||||
</div>
|
@ -26,13 +26,14 @@
|
||||
* to support and display images related content.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.app.core.images', [])
|
||||
.module('horizon.app.core.images', ['ngRoute'])
|
||||
.constant('horizon.app.core.images.events', events())
|
||||
.config(config);
|
||||
|
||||
config.$inject = [
|
||||
'$provide',
|
||||
'$windowProvider'
|
||||
'$windowProvider',
|
||||
'$routeProvider'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -48,12 +49,31 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.images.basePath
|
||||
* @description Base path for the images code
|
||||
* @name horizon.app.core.images.tableRoute
|
||||
* @name horizon.app.core.images.detailsRoute
|
||||
* @description Routes used by this module.
|
||||
*/
|
||||
function config($provide, $windowProvider) {
|
||||
function config($provide, $windowProvider, $routeProvider) {
|
||||
var path = $windowProvider.$get().STATIC_URL + 'app/core/images/';
|
||||
$provide.constant('horizon.app.core.images.basePath', path);
|
||||
var webroot = $windowProvider.$get().WEBROOT;
|
||||
var tableUrl = path + "table/";
|
||||
var projectTableRoute = webroot + 'project/ngimages/';
|
||||
var detailsUrl = path + "detail/";
|
||||
var projectDetailsRoute = webroot + 'project/ngimages/details/';
|
||||
|
||||
// Share the routes as constants so that views within the images module
|
||||
// can create links to each other.
|
||||
$provide.constant('horizon.app.core.images.tableRoute', projectTableRoute);
|
||||
$provide.constant('horizon.app.core.images.detailsRoute', projectDetailsRoute);
|
||||
|
||||
$routeProvider
|
||||
.when(projectTableRoute, {
|
||||
templateUrl: tableUrl + 'images-table.html'
|
||||
})
|
||||
.when(projectDetailsRoute + ':imageId', {
|
||||
templateUrl: detailsUrl + 'image-detail.html'
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -22,23 +22,76 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizon.app.core.images.basePath constant', function () {
|
||||
var imagesBasePath, staticUrl;
|
||||
describe('horizon.app.core.images.tableRoute constant', function () {
|
||||
var tableRoute, webRoot;
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
imagesBasePath = $injector.get('horizon.app.core.images.basePath');
|
||||
staticUrl = $injector.get('$window').STATIC_URL;
|
||||
tableRoute = $injector.get('horizon.app.core.images.tableRoute');
|
||||
webRoot = $injector.get('$window').WEBROOT;
|
||||
}));
|
||||
|
||||
it('should be defined', function () {
|
||||
expect(imagesBasePath).toBeDefined();
|
||||
expect(tableRoute).toBeDefined();
|
||||
});
|
||||
|
||||
it('should equal to "/static/app/core/images/"', function () {
|
||||
expect(imagesBasePath).toEqual(staticUrl + 'app/core/images/');
|
||||
it('should equal to "/project/ngimages/"', function () {
|
||||
expect(tableRoute).toEqual(webRoot + 'project/ngimages/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizon.app.core.images.detailsRoute constant', function () {
|
||||
var detailsRoute, webRoot;
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
detailsRoute = $injector.get('horizon.app.core.images.detailsRoute');
|
||||
webRoot = $injector.get('$window').WEBROOT;
|
||||
}));
|
||||
|
||||
it('should be defined', function () {
|
||||
expect(detailsRoute).toBeDefined();
|
||||
});
|
||||
|
||||
it('should equal to "/project/ngimages/details/"', function () {
|
||||
expect(detailsRoute).toEqual(webRoot + 'project/ngimages/details/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('$routeProvider should be configured for images', function() {
|
||||
var staticUrl, $routeProvider;
|
||||
|
||||
beforeEach(function() {
|
||||
module('ngRoute');
|
||||
angular.module('routeProviderConfig', [])
|
||||
.config(function(_$routeProvider_) {
|
||||
$routeProvider = _$routeProvider_;
|
||||
spyOn($routeProvider, 'when').and.callThrough();
|
||||
});
|
||||
|
||||
module('routeProviderConfig');
|
||||
module('horizon.app.core');
|
||||
|
||||
inject(function ($injector) {
|
||||
staticUrl = $injector.get('$window').STATIC_URL;
|
||||
});
|
||||
});
|
||||
|
||||
it('should set table and detail path', function() {
|
||||
expect($routeProvider.when.calls.count()).toEqual(2);
|
||||
var imagesRouteCallArgs = $routeProvider.when.calls.argsFor(0);
|
||||
expect(imagesRouteCallArgs).toEqual([
|
||||
'/project/ngimages/', {templateUrl: staticUrl + 'app/core/images/table/images-table.html'}
|
||||
]);
|
||||
var imagesDetailsCallArgs = $routeProvider.when.calls.argsFor(1);
|
||||
expect(imagesDetailsCallArgs).toEqual([
|
||||
'/project/ngimages/details/:imageId',
|
||||
{ templateUrl: staticUrl + 'app/core/images/detail/image-detail.html'}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -7,7 +7,6 @@
|
||||
default-sort="name"
|
||||
default-sort-reverse="false"
|
||||
class="table-striped table-rsp table-detail modern">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<!--
|
||||
@ -68,7 +67,7 @@
|
||||
duration="200">
|
||||
</span>
|
||||
</td>
|
||||
<td class="rsp-p1">{$ image.name $}</td>
|
||||
<td class="rsp-p1"><a ng-href="{$ table.detailsRoute + image.id $}">{$ image.name $}</a></td>
|
||||
<td class="rsp-p1">{$ image | imageType $}</td>
|
||||
<td class="rsp-p1">{$ image.status | imageStatus $}</td>
|
||||
<td class="rsp-p2">{$ image.filtered_visibility $}</td>
|
||||
@ -111,7 +110,7 @@
|
||||
</dl>
|
||||
<dl class="col-sm-2">
|
||||
<dt translate>Format</dt>
|
||||
<dd>{$ image.disk_format | noValue $}</dd>
|
||||
<dd>{$ image.disk_format | noValue | uppercase $}</dd>
|
||||
</dl>
|
||||
<dl class="col-sm-2">
|
||||
<dt translate>Size</dt>
|
||||
|
@ -24,6 +24,7 @@
|
||||
ImagesTableController.$inject = [
|
||||
'$q',
|
||||
'$scope',
|
||||
'horizon.app.core.images.detailsRoute',
|
||||
'horizon.app.core.images.table.batch-actions.service',
|
||||
'horizon.app.core.images.table.row-actions.service',
|
||||
'horizon.app.core.images.events',
|
||||
@ -43,6 +44,7 @@
|
||||
function ImagesTableController(
|
||||
$q,
|
||||
$scope,
|
||||
detailsRoute,
|
||||
batchActionsService,
|
||||
rowActionsService,
|
||||
events,
|
||||
@ -52,6 +54,8 @@
|
||||
) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.detailsRoute = detailsRoute;
|
||||
|
||||
ctrl.checked = {};
|
||||
|
||||
ctrl.images = [];
|
||||
|
@ -53,7 +53,7 @@
|
||||
2: {id: '2', is_public: false, owner: 'not_me', filtered_visibility: 'Shared with Me'}
|
||||
};
|
||||
|
||||
var $scope, controller, events;
|
||||
var $scope, controller, events, detailsRoute;
|
||||
|
||||
beforeEach(module('ui.bootstrap'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
@ -71,6 +71,7 @@
|
||||
$scope = _$rootScope_.$new();
|
||||
events = $injector.get('horizon.app.core.images.events');
|
||||
controller = $injector.get('$controller');
|
||||
detailsRoute = $injector.get('horizon.app.core.images.detailsRoute');
|
||||
|
||||
spyOn(glanceAPI, 'getImages').and.callThrough();
|
||||
spyOn(userSession, 'get').and.callThrough();
|
||||
@ -87,6 +88,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
it('should set details route properly', function() {
|
||||
expect(createController().detailsRoute).toEqual(detailsRoute);
|
||||
});
|
||||
|
||||
it('should invoke initialization apis', function() {
|
||||
var ctrl = createController();
|
||||
expect(userSession.get).toHaveBeenCalled();
|
||||
|
Loading…
x
Reference in New Issue
Block a user