Adding Angular Project Images Table

This patch adds the images table to the projects panel.

It follows the pattern established by the following review:
https://review.openstack.org/#/c/197373/

However, it pre-fixes the table files with images- to make them
easier to find via search in tools like the pycharms debugger
and it the javascript console on various browsers.

To be added in subsequent patches:
 - Actions
 - Forms
 - Common images table directive (TBD)
 - Filters via Magic Search
 - Integration to Searchlight

To test set DISABLED = False in _203_project_images_panel.py

Change-Id: I5e509fb807c356d3fb4b58e974ffa160828dbf88
Partially-Implements: blueprint angularize-images-table
This commit is contained in:
Travis Tripp 2015-07-09 23:53:28 -06:00
parent e62918d706
commit eaf27075b6
25 changed files with 730 additions and 6 deletions

View File

@ -288,9 +288,11 @@ $em-per-priority: floor($table-col-avg-width / $font-size-base) * 3;
display: none;
}
.rsp-alt-p1, .rsp-alt-p2,
.rsp-alt-p3, .rsp-alt-p4 {
display: inline-block;
th,td {
&.rsp-alt-p1, &.rsp-alt-p2,
&.rsp-alt-p3, &.rsp-alt-p4 {
display: inline-block;
}
}
}

View File

@ -8,4 +8,5 @@
{% endblock page_header %}
{% block main %}
<ng-include src="'{{ STATIC_URL }}app/core/images/table/images-table.html'"></ng-include>
{% endblock %}

View File

@ -0,0 +1,46 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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 hz.dashboard.project.images
*
* @description
* Provides the services and widgets required
* to support and display the project images panel.
*/
angular
.module('hz.dashboard.project.images', [])
.config(config);
config.$inject = [
'$provide',
'$windowProvider'
];
/**
* @name hz.dashboard.project.images.basePath
* @description Base path for the project dashboard
*/
function config($provide, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'dashboard/project/images/';
$provide.constant('hz.dashboard.project.images.basePath', path);
}
})();

View File

@ -0,0 +1,45 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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('hz.dashboard.project.images', function() {
it('should exist', function() {
expect(angular.module('hz.dashboard.project.images')).toBeDefined();
});
});
describe('hz.dashboard.project.images.basePath constant', function () {
var imagesBasePath, staticUrl;
beforeEach(module('hz.dashboard'));
beforeEach(module('hz.dashboard.project'));
beforeEach(module('hz.dashboard.project.images'));
beforeEach(inject(function ($injector) {
imagesBasePath = $injector.get('hz.dashboard.project.images.basePath');
staticUrl = $injector.get('$window').STATIC_URL;
}));
it('should be defined', function () {
expect(imagesBasePath).toBeDefined();
});
it('should equal to "/static/dashboard/project/images/"', function () {
expect(imagesBasePath).toEqual(staticUrl + 'dashboard/project/images/');
});
});
})();

View File

@ -18,12 +18,28 @@
'use strict';
/**
* @ngdoc hz.dashboard.project
* @ngModule
* @ngdoc overview
* @ngname hz.dashboard.project
*
* @description
* Dashboard module to host project panels.
*/
angular
.module('hz.dashboard.project', []);
.module('hz.dashboard.project', ['hz.dashboard.project.images'])
.config(config);
config.$inject = [
'$provide',
'$windowProvider'
];
/**
* @name hz.dashboard.project.basePath
* @description Base path for the project dashboard
*/
function config($provide, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'dashboard/project/';
$provide.constant('hz.dashboard.project.basePath', path);
}
})();

View File

@ -22,4 +22,23 @@
});
});
describe('hz.dashboard.project.basePath constant', function () {
var projectBasePath, staticUrl;
beforeEach(module('hz.dashboard'));
beforeEach(module('hz.dashboard.project'));
beforeEach(inject(function ($injector) {
projectBasePath = $injector.get('hz.dashboard.project.basePath');
staticUrl = $injector.get('$window').STATIC_URL;
}));
it('should be defined', function () {
expect(projectBasePath).toBeDefined();
});
it('should equal to "/static/dashboard/project/"', function () {
expect(projectBasePath).toEqual(staticUrl + 'dashboard/project/');
});
});
})();

View File

@ -28,6 +28,7 @@
*/
angular
.module('horizon.app.core', [
'horizon.app.core.images',
'horizon.app.core.workflow'
]);

View File

@ -0,0 +1 @@
@import "images/images";

View File

@ -0,0 +1,49 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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')
.filter('imageStatus', imageStatusFilter);
imageStatusFilter.$inject = [
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc filter
* @name imageStatusFilter
* @description
* Takes raw image status from the API and returns the user friendly status.
*/
function imageStatusFilter(gettext) {
var imageStatuses = {
'active': gettext('Active'),
'saving': gettext('Saving'),
'queued': gettext('Queued'),
'pending_delete': gettext('Pending Delete'),
'killed': gettext('Killed'),
'deleted': gettext('Deleted')
};
return function (input) {
var result = imageStatuses[input];
return angular.isDefined(result) ? result : input;
};
}
}());

View File

@ -0,0 +1,40 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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.imageStatus Filter', function () {
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.app.core.images'));
describe('iumageStatus', function () {
var imageStatusFilter;
beforeEach(inject(function (_imageStatusFilter_) {
imageStatusFilter = _imageStatusFilter_;
}));
it('Returns value when key is present', function () {
expect(imageStatusFilter('active')).toBe('Active');
});
it('Returns input when key is not present', function () {
expect(imageStatusFilter('unknown')).toBe('unknown');
});
});
});
})();

View File

@ -0,0 +1,46 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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')
.filter('imageType', imageTypeFilter);
imageTypeFilter.$inject = [
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc filter
* @name imageTypeFilter
* @description
* Takes a raw image object from the API and returns the user friendly type.
*/
function imageTypeFilter(gettext) {
return function (input) {
if (null !== input &&
angular.isDefined(input) &&
angular.isDefined(input.properties) &&
input.properties.image_type === 'snapshot') {
return gettext('Snapshot');
} else {
return gettext('Image');
}
};
}
}());

View File

@ -0,0 +1,48 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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.imageType Filter', function () {
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.app.core.images'));
describe('imageType', function () {
var imageTypeFilter;
beforeEach(inject(function (_imageTypeFilter_) {
imageTypeFilter = _imageTypeFilter_;
}));
it('returns Snapshot for snapshot', function () {
expect(imageTypeFilter({properties:{image_type:'snapshot'}})).toBe('Snapshot');
});
it('returns Image for image', function () {
expect(imageTypeFilter({properties:{image_type:'image'}})).toBe('Image');
});
it('returns Image for null', function () {
expect(imageTypeFilter(null)).toBe('Image');
});
it('returns Image for undefined', function () {
expect(imageTypeFilter(undefined)).toBe('Image');
});
});
});
})();

View File

@ -0,0 +1,46 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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.app.core.images
*
* @description
* Provides all of the services and widgets required
* to support and display images related content.
*/
angular
.module('horizon.app.core.images', [])
.config(config);
config.$inject = [
'$provide',
'$windowProvider'
];
/**
* @name horizon.app.core.images.basePath
* @description Base path for the images code
*/
function config($provide, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'app/core/images/';
$provide.constant('horizon.app.core.images.basePath', path);
}
})();

View File

@ -0,0 +1,44 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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 () {
it('should exist', function () {
expect(angular.module('horizon.app.core.images')).toBeDefined();
});
});
describe('horizon.app.core.images.basePath constant', function () {
var imagesBasePath, staticUrl;
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;
}));
it('should be defined', function () {
expect(imagesBasePath).toBeDefined();
});
it('should equal to "/static/app/core/images/"', function () {
expect(imagesBasePath).toEqual(staticUrl + 'app/core/images/');
});
});
})();

View File

@ -0,0 +1,11 @@
table[ng-controller="imagesTableController as table"] {
.detail-expanded .row {
background: none;
padding-left: 2em;
}
&.table-rsp .action-col {
min-width: 12em;
}
}

View File

@ -0,0 +1,10 @@
<!--
Table-batch-actions:
This is where batch actions like searching, creating, and deleting.
-->
<th colspan="100" class="search-header">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
</hz-search-bar>
</th>

View File

@ -0,0 +1,10 @@
<!--
Table-footer:
This is where we display number of items and pagination controls.
Ensure colspan is greater or equal to number of column-headers.
-->
<td colspan="100">
<span class="display">{$ table.images.length|itemCount $}</span>
<div st-pagination="" st-items-by-page="10" st-displayed-pages="10"></div>
</td>

View File

@ -0,0 +1,41 @@
<!--
Table-column-headers:
This is where we declaratively define the table column headers.
Include select-col if you want to select all.
Include expander if you want to inline details.
Include action-col if you want to perform actions.
-->
<th class="select-col">
<input type="checkbox" hz-select-all="table.images">
</th>
<th class="expander"></th>
<th class="rsp-p1" st-sort="name" st-sort-default="name">
<translate>Image Name</translate>
</th>
<th class="rsp-p1" st-sort="type">
<translate>Type</translate>
</th>
<th class="rsp-p1" st-sort="status">
<translate>Status</translate>
</th>
<th class="rsp-p2" st-sort="is_public">
<translate>Public</translate>
</th>
<th class="rsp-p2" st-sort="protected">
<translate>Protected</translate>
</th>
<th class="rsp-p2" st-sort="disk_format">
<translate>Format</translate>
</th>
<th class="rsp-p2" st-sort="size">
<translate>Size</translate>
</th>

View File

@ -0,0 +1,7 @@
<!--
Table-row-action-column:
Actions taken here applies to a single item/row.
-->
<action-list dropdown>
</action-list>

View File

@ -0,0 +1,55 @@
<!--
Detail-row:
Contains detailed information on this item.
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
-->
<td class="detail" colspan="100">
<div class="row">
<span class="rsp-alt-p2">
<dl class="col-sm-2">
<dt translate>Public</dt>
<dd>
{$ image.is_public | yesno $}
</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Protected</dt>
<dd>
{$ image.protected | yesno $}
</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Format</dt>
<dd>
{$ image.disk_format || '--' $}
</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Size</dt>
<dd>
{$ image.size | bytes $}
</dd>
</dl>
</span>
</div>
<div class="row">
<dl class="col-sm-2">
<dt translate>Min Disk (GB)</dt>
<dd>
{$ image.min_disk || '--' $}
</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Min RAM (MB)</dt>
<dd>
{$ image.min_ram || '--' $}
</dd>
</dl>
</div>
<!-- TODO Add in metadata defs from launch instance image details -->
</td>

View File

@ -0,0 +1,38 @@
<!--
Table-rows:
This is where we declaratively define the table columns.
Include select-col if you want to select all.
Include expander if you want to inline details.
Include action-col if you want to perform actions.
rsp-p1 rsp-p2 are responsive priority as user resizes window.
-->
<tr ng-repeat-start="image in table.images track by image.id"
ng-class="{'st-selected': checked[image.id]}">
<td class="select-col">
<input type="checkbox"
ng-model="selected[image.id].checked"
hz-select="image">
</td>
<td class="expander">
<i class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</i>
</td>
<td class="rsp-p1">{$ image.name $}</td>
<td class="rsp-p1">{$ image | imageType $}</td>
<td class="rsp-p1">{$ image.status | imageStatus $}</td>
<td class="rsp-p2">{$ image.is_public | yesno $}</td>
<td class="rsp-p2">{$ image.protected | yesno $}</td>
<td class="rsp-p2">{$ image.disk_format || '--' $}</td>
<td class="rsp-p2">{$ image.size | bytes $}</td>
</tr>
<tr ng-repeat-end class="detail-row"
ng-include="table.path + 'images-table-row-details.html'">
</tr>

View File

@ -0,0 +1,61 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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 ImagesTableController
*
* @description
* Controller for the images table.
* Serves as the focal point for table actions.
*/
angular
.module('horizon.app.core.images')
.controller('imagesTableController', ImagesTableController);
ImagesTableController.$inject = [
'horizon.app.core.images.basePath',
'horizon.openstack-service-api.glance'
];
function ImagesTableController(basepath, glance) {
var ctrl = this;
ctrl.images = [];
ctrl.imagesSrc = [];
ctrl.checked = {};
ctrl.path = basepath + 'table/';
init();
////////////////////////////////
function init() {
// if user has permission
// fetch table data and populate it
glance.getImages().success(onGetImages);
}
function onGetImages(response) {
ctrl.imagesSrc = response.items;
}
}
})();

View File

@ -0,0 +1,68 @@
/**
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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 table controller', function() {
function fakeGlance() {
return {
success: function(callback) {
callback({
items : []
});
}
};
}
var controller, glanceAPI, staticUrl;
///////////////////////
beforeEach(module('horizon.framework.util.http'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.openstack-service-api'));
beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.app.core.images'));
beforeEach(inject(function($injector) {
glanceAPI = $injector.get('horizon.openstack-service-api.glance');
controller = $injector.get('$controller');
staticUrl = $injector.get('$window').STATIC_URL;
spyOn(glanceAPI, 'getImages').and.callFake(fakeGlance);
}));
function createController() {
return controller('imagesTableController', {
glanceAPI: glanceAPI
});
}
it('should set path properly', function() {
var path = staticUrl + 'app/core/images/table/';
expect(createController().path).toEqual(path);
});
it('should invoke glance apis', function() {
createController();
expect(glanceAPI.getImages).toHaveBeenCalled();
});
});
})();

View File

@ -0,0 +1,18 @@
<table ng-controller="imagesTableController as table"
hz-table ng-cloak
st-table="table.images"
st-safe-src="table.imagesSrc"
default-sort="name"
default-sort-reverse="false"
class="table-striped table-rsp table-detail modern">
<thead>
<tr ng-include="table.path + 'images-table-batch-actions.html'"></tr>
<tr ng-include="table.path + 'images-table-header.html'"></tr>
</thead>
<tbody ng-include="table.path + 'images-table-rows.html'"></tbody>
<tfoot>
<tr ng-include="table.path + 'images-table-footer.html'"></tr>
</tfoot>
</table>

View File

@ -3,6 +3,7 @@
@import "scss/variables";
@import "launch-instance/launch-instance";
@import "/app/core/core";
// Custom Style Variables
@import "/custom/styles";