Add angular flavors panel

Adding a new panel to the admin dashboard that will use angular js
instead of the django templates

To test set DISABLED = False in _2081_admin_flavors_panel.py

Co-Authored-By: Rajat Vig<rajatv@thoughtworks.com>
Co-Authored-By: Errol Pais<epais@thoughtworks.com>
Co-Authored-By: Kristine Brown<kbrown@thoughtworks.com>

Change-Id: I9394ddfe3791aeb7a52194f37e1e668e33c0325b
Partially-Implements: blueprint ng-flavors
This commit is contained in:
Rajat Vig 2015-10-12 23:31:15 -07:00
parent e62cdfaadb
commit 65c4895c36
16 changed files with 448 additions and 0 deletions

View File

@ -51,8 +51,14 @@
*/ */
function gbFilter() { function gbFilter() {
return function (input) { return function (input) {
var tb = 1024;
if (isNaN(input) || null === input) { if (isNaN(input) || null === input) {
return ''; return '';
} else if (input >= tb) {
return interpolate(gettext("%s TB"), [parseFloat(Number(input / tb).toFixed(2))]);
} else if (input === '') {
return interpolate(gettext("0 GB"));
} else { } else {
return interpolate(gettext("%s GB"), [input.toString()]); return interpolate(gettext("%s GB"), [input.toString()]);
} }
@ -68,8 +74,14 @@
*/ */
function mbFilter() { function mbFilter() {
return function (input) { return function (input) {
var gb = 1024;
if (isNaN(input) || null === input) { if (isNaN(input) || null === input) {
return ''; return '';
} else if (input >= gb) {
return interpolate(gettext("%s GB"), [parseFloat(Number(input / gb).toFixed(2))]);
} else if (input === '') {
return interpolate(gettext("0 MB"));
} else { } else {
return interpolate(gettext("%s MB"), [input.toString()]); return interpolate(gettext("%s MB"), [input.toString()]);
} }

View File

@ -47,6 +47,7 @@
it('returns given numeric value properly', function () { it('returns given numeric value properly', function () {
expect(gbFilter(12)).toBe('12 GB'); expect(gbFilter(12)).toBe('12 GB');
expect(gbFilter(1200)).toBe('1.17 TB');
expect(gbFilter(-12)).toBe('-12 GB'); expect(gbFilter(-12)).toBe('-12 GB');
expect(gbFilter(12.12)).toBe('12.12 GB'); expect(gbFilter(12.12)).toBe('12.12 GB');
}); });
@ -68,6 +69,7 @@
it('returns given numeric value properly', function () { it('returns given numeric value properly', function () {
expect(mbFilter(12)).toBe('12 MB'); expect(mbFilter(12)).toBe('12 MB');
expect(mbFilter(1200)).toBe('1.17 GB');
expect(mbFilter(-12)).toBe('-12 MB'); expect(mbFilter(-12)).toBe('-12 MB');
expect(mbFilter(12.12)).toBe('12.12 MB'); expect(mbFilter(12.12)).toBe('12.12 MB');
}); });

View File

@ -0,0 +1,24 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
# (c) Copyright 2015 ThoughtWorks, 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.
from django.utils.translation import ugettext_lazy as _
import horizon
class NGFlavors(horizon.Panel):
name = _("Flavors")
slug = 'ngflavors'
permissions = ('openstack.services.compute',)

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Flavors" %}{% endblock %}
{% block page_header %}
<hz-page-header header="'{% trans "Flavors" %}'"></hz-page-header>
{% endblock %}
{% block main %}
<ng-include src="'{{ STATIC_URL }}dashboard/admin/flavors/table/flavors-table.html'"></ng-include>
{% endblock %}

View File

@ -0,0 +1,25 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
# (c) Copyright 2015 ThoughtWorks, 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.
from django.conf.urls import patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.admin.ngflavors import views
urlpatterns = patterns(
'openstack_dashboard.dashboards.admin.ngflavors.views',
url(r'^$', views.IndexView.as_view(), name='index'),
)

View File

@ -0,0 +1,19 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
# (c) Copyright 2015 ThoughtWorks, 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.
import horizon.views as views
class IndexView(views.HorizonTemplateView):
template_name = 'admin/ngflavors/index.html'

View File

@ -25,6 +25,7 @@
*/ */
angular angular
.module('horizon.dashboard.admin', [ .module('horizon.dashboard.admin', [
'horizon.dashboard.admin.flavors'
]) ])
.config(config); .config(config);

View File

@ -0,0 +1,44 @@
/**
*
* 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.admin.flavors')
.filter('hasExtras', hasExtrasFilter);
hasExtrasFilter.$inject = [];
/**
* @ngdoc filter
* @name hasExtrasFilter
* @description
* If input is defined and has more than one property return 'Yes' else return 'No'
*
*/
function hasExtrasFilter(gettext) {
return function check(input) {
if (input &&
angular.isObject(input) &&
!angular.isArray(input) &&
Object.keys(input).length > 0) {
return true;
}
return false;
};
}
})();

View File

@ -0,0 +1,43 @@
/**
*
* 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.admin.flavors.hasExtras', function() {
var hasExtras;
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.admin.flavors'));
beforeEach(inject(function(_hasExtrasFilter_) {
hasExtras = _hasExtrasFilter_;
}));
it('returns Yes when key is present', function() {
var input = { 1: 'test' };
expect(hasExtras(input)).toBeTruthy();
});
it('returns No when object is undefined or has no properties', function() {
expect(hasExtras(undefined)).not.toBeTruthy();
expect(hasExtras({})).not.toBeTruthy();
expect(hasExtras('string')).not.toBeTruthy();
expect(hasExtras(1)).not.toBeTruthy();
expect(hasExtras([1])).not.toBeTruthy();
});
});
})();

View File

@ -0,0 +1,30 @@
/**
*
* 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 horizon.dashboard.admin.flavors
* @ngModule
*
* @description
* Provides all of the services and widgets required
* to support and display the flavors panel.
*/
angular
.module('horizon.dashboard.admin.flavors', []);
})();

View File

@ -0,0 +1,24 @@
/**
*
* 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.admin.flavors', function () {
it('should exist', function () {
expect(angular.module('horizon.dashboard.admin.flavors')).toBeDefined();
});
});
})();

View File

@ -0,0 +1,56 @@
/**
*
* 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.admin.flavors')
.controller('FlavorsTableController', FlavorsTableController);
FlavorsTableController.$inject = [
'horizon.app.core.openstack-service-api.nova'
];
/**
* @ngdoc FlavorsTableController
* @ngController
*
* @description
* Controller for the flavors panel.
* Serves as the focal point for table actions.
*/
function FlavorsTableController(
nova
) {
var ctrl = this;
ctrl.flavors = [];
ctrl.iflavors = [];
init();
////////////////////////////////
function init() {
nova.getFlavors(true, true).then(onGetFlavors);
}
function onGetFlavors(response) {
ctrl.flavors = response.data.items;
}
}
})();

View File

@ -0,0 +1,67 @@
/**
*
* 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.admin.flavors.controller.FlavorsTableController', function () {
var flavors = [{id: '1'}, {id: '2'}];
var novaAPI = {
getFlavors: function(params) {
var deferred = $q.defer();
deferred.resolve({data: {items: flavors}});
return deferred.promise;
}
};
var controller, $q, $scope;
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.app.core.openstack-service-api', function($provide) {
$provide.value('horizon.app.core.openstack-service-api.nova', novaAPI);
}));
beforeEach(module('horizon.dashboard.admin', function($provide) {
$provide.constant('horizon.dashboard.admin.basePath', '/a/sample/path/');
}));
beforeEach(module('horizon.dashboard.admin.flavors'));
beforeEach(inject(function ($injector, _$rootScope_) {
$scope = _$rootScope_.$new();
$q = $injector.get('$q');
controller = $injector.get('$controller');
}));
function createController() {
return controller('FlavorsTableController', {});
}
it('should invoke nova apis', function() {
spyOn(novaAPI, 'getFlavors').and.callThrough();
var ctrl = createController();
$scope.$apply();
expect(novaAPI.getFlavors).toHaveBeenCalled();
expect(ctrl.flavors).toEqual(flavors);
expect(ctrl.iflavors).toBeDefined();
});
});
})();

View File

@ -0,0 +1,59 @@
<table ng-controller="FlavorsTableController as table"
hz-table ng-cloak
st-table="table.iflavors"
st-safe-src="table.flavors"
default-sort="ram"
default-sort-reverse="false"
class="table-striped table-rsp table-detail modern">
<thead>
<tr>
<th colspan="100" class="search-header">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
</hz-search-bar>
</th>
</tr>
<tr>
<th class="select-col">
<input type="checkbox" hz-select-all="table.iflavors">
</th>
<th class="rsp-p1" st-sort="name" translate>Flavor Name</th>
<th class="rsp-p1" st-sort="vcpus" translate>VCPUs</th>
<th class="rsp-p1" st-sort="ram" st-sort-default="ram" translate>RAM</th>
<th class="rsp-p2" st-sort="disk" translate>Root Disk</th>
<th class="rsp-p2" st-sort="ephermal" translate>Ephemeral Disk</th>
<th class="rsp-p2" st-sort="swap" translate>Swap Disk</th>
<th class="rsp-p2" st-sort="id" translate>ID</th>
<th class="rsp-p2" st-sort="is_public" translate>Public</th>
<th class="rsp-p2" st-sort="metadata" translate>Metadata</th>
<th class="rsp-p2"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="flavor in table.iflavors track by flavor.id">
<td class="select-col">
<input type="checkbox"
ng-model="selected[flavor.id].checked"
hz-select="flavor">
</td>
<td class="rsp-p1">{$ flavor.name $}</td>
<td class="rsp-p1">{$ flavor.vcpus $}</td>
<td class="rsp-p1">{$ flavor.ram | mb $}</td>
<td class="rsp-p2">{$ flavor.disk | gb $}</td>
<td class="rsp-p2">{$ flavor.ephemeral | gb $}</td>
<td class="rsp-p2">{$ flavor.swap | mb $}</td>
<td class="rsp-p2">{$ flavor.id $}</td>
<td class="rsp-p2">{$ flavor.is_public | yesno $}</td>
<td class="rsp-p2">{$ flavor.extras | hasExtras | yesno $}</td>
<td class="rsp-p2">
</td>
</tr>
</tbody>
<tfoot hz-table-footer items="table.iflavors"></tfoot>
</table>

View File

@ -0,0 +1,31 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
# (c) Copyright 2015 ThoughtWorks, 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.
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
# If you want the panel to show up without a panel group,
# use the panel group "default".
PANEL_GROUP = 'admin'
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'ngflavors'
# If set to True, this settings file will not be added to the settings.
DISABLED = True
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.admin.ngflavors.panel.NGFlavors'