Flavors panel can switch to Angular

This patch enables all of the features for the Flavors panel to use Angular but
disables it, so that it is easy to switch on/off.

Note that we add integration test switches since it can't read the Django
conf.

Note that I changed the common tests to allow for testing of api calls that
don't produce error toasts, because we needed better branch coverage, and
the deleteFlavor api wasn't fully branch-tested.

Change-Id: I92b1b57bd486e5eb87179cb8d44b7551e9de2e0f
Partially-Implements: blueprint ng-flavors
This commit is contained in:
Matt Borland 2016-07-18 13:50:00 -06:00 committed by Cindy Lu
parent dd6c644755
commit e8e84c2db4
31 changed files with 333 additions and 508 deletions

View File

@ -432,7 +432,8 @@ This example sorts flavors by vcpus in descending order::
Default:: Default::
{ {
'images_panel': True 'images_panel': True,
'flavors_panel': False,
} }
A dictionary of currently available AngularJS features. This allows simple A dictionary of currently available AngularJS features. This allows simple

View File

@ -16,13 +16,24 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from openstack_dashboard.dashboards.admin.flavors import views from openstack_dashboard.dashboards.admin.flavors import views
urlpatterns = [ if settings.ANGULAR_FEATURES['flavors_panel']:
url(r'^$', views.IndexView.as_view(), name='index'), # New angular panel
url(r'^create/$', views.CreateView.as_view(), name='create'), urlpatterns = [
url(r'^(?P<id>[^/]+)/update/$', views.UpdateView.as_view(), name='update'), url(r'^$', views.AngularIndexView.as_view(), name='index'),
] url(r'^create/$', views.AngularIndexView.as_view(), name='create'),
url(r'^(?P<id>[^/]+)/update/$', views.AngularIndexView.as_view(),
name='index'),
]
else:
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
]

View File

@ -18,6 +18,7 @@
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import exceptions from horizon import exceptions
from horizon import tables from horizon import tables
@ -34,6 +35,10 @@ from openstack_dashboard.dashboards.admin.flavors \
INDEX_URL = "horizon:admin:flavors:index" INDEX_URL = "horizon:admin:flavors:index"
class AngularIndexView(generic.TemplateView):
template_name = 'angular.html'
class IndexView(tables.DataTableView): class IndexView(tables.DataTableView):
table_class = project_tables.FlavorsTable table_class = project_tables.FlavorsTable
template_name = 'admin/flavors/index.html' template_name = 'admin/flavors/index.html'

View File

@ -1,46 +0,0 @@
/**
*
* 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
* @ngModule
*
* @description
* Dashboard module to host various admin panels.
*/
angular
.module('horizon.dashboard.admin', [
'horizon.dashboard.admin.flavors'
])
.config(config);
config.$inject = [
'$provide',
'$windowProvider'
];
/**
* @name horizon.dashboard.admin.basePath
* @description Base path for the admin dashboard
*/
function config($provide, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'dashboard/admin/';
$provide.constant('horizon.dashboard.admin.basePath', path);
}
})();

View File

@ -1,38 +0,0 @@
/**
*
* 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', function() {
var staticUrl, basePath;
beforeEach(module('horizon.dashboard.admin'));
beforeEach(inject(function($injector) {
staticUrl = $injector.get('$window').STATIC_URL;
basePath = $injector.get('horizon.dashboard.admin.basePath');
}));
it('should exist', function() {
expect(angular.module('horizon.dashboard.admin')).toBeDefined();
});
it('should set path properly', function () {
expect(basePath).toEqual(staticUrl + 'dashboard/admin/');
});
});
})();

View File

@ -1,44 +0,0 @@
/**
*
* 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() {
return function check(input) {
if (input &&
angular.isObject(input) &&
!angular.isArray(input) &&
Object.keys(input).length > 0) {
return true;
}
return false;
};
}
})();

View File

@ -1,43 +0,0 @@
/**
*
* 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()).not.toBeTruthy();
expect(hasExtras({})).not.toBeTruthy();
expect(hasExtras('string')).not.toBeTruthy();
expect(hasExtras(1)).not.toBeTruthy();
expect(hasExtras([1])).not.toBeTruthy();
});
});
})();

View File

@ -1,30 +0,0 @@
/**
*
* 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

@ -1,24 +0,0 @@
/**
*
* 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

@ -1,87 +0,0 @@
/**
*
* 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 = [];
ctrl.searchFacets = getSearchFacets();
init();
////////////////////////////////
function init() {
nova.getFlavors(true, true).then(onGetFlavors);
}
function onGetFlavors(response) {
ctrl.flavors = response.data.items;
}
function getSearchFacets() {
return [
{
label: gettext('Name'),
name: 'name',
singleton: true
},
{
label: gettext('VCPUs'),
name: 'vcpus',
singleton: true
},
{
label: gettext('RAM'),
name: 'ram',
singleton: true
},
{
label: gettext('Public'),
name: 'os-flavor-access:is_public',
singleton: true,
options: [
{label: gettext('Yes'), key: 'true'},
{label: gettext('No'), key: 'false'}
]
}
];
}
}
})();

View File

@ -1,77 +0,0 @@
/**
*
* 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() {
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 set facets for search', function () {
var ctrl = createController();
expect(ctrl.searchFacets).toBeDefined();
expect(ctrl.searchFacets.length).toEqual(4);
expect(ctrl.searchFacets[0].name).toEqual('name');
expect(ctrl.searchFacets[1].name).toEqual('vcpus');
expect(ctrl.searchFacets[2].name).toEqual('ram');
expect(ctrl.searchFacets[3].name).toEqual('os-flavor-access:is_public');
});
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

@ -1,63 +0,0 @@
<div ng-controller="FlavorsTableController as table">
<hz-magic-search-context filter-facets="table.searchFacets">
<hz-magic-search-bar>
</hz-magic-search-bar>
<table st-magic-search
hz-table ng-cloak
st-table="table.iflavors"
st-safe-src="table.flavors"
class="table table-striped table-rsp table-detail">
<thead>
<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="rxtx_factor" translate>RX/TX factor</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="tCtrl.selections[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.rxtx_factor $}</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>
<tr hz-no-items items="table.iflavors"></tr>
</tbody>
<tfoot hz-table-footer items="table.iflavors"></tfoot>
</table>
</hz-magic-search-context>
</div>

View File

@ -21,7 +21,6 @@ ADD_INSTALLED_APPS = [
] ]
ADD_ANGULAR_MODULES = [ ADD_ANGULAR_MODULES = [
'horizon.dashboard.admin',
] ]
AUTO_DISCOVER_STATIC_FILES = True AUTO_DISCOVER_STATIC_FILES = True

View File

@ -1,3 +1,18 @@
# (c) Copyright 2016 Hewlett Packard Enterprise Development LP
#
# 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 panel to be added to HORIZON_CONFIG. Required. # The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'flavors' PANEL = 'flavors'
# The slug of the dashboard the PANEL associated with. Required. # The slug of the dashboard the PANEL associated with. Required.

View File

@ -1,31 +0,0 @@
# (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'

View File

@ -2,4 +2,4 @@
# wanting to only test legacy panels. Since 'local' modules are evaluated # wanting to only test legacy panels. Since 'local' modules are evaluated
# after settings.py, these configurations will override the default settings. # after settings.py, these configurations will override the default settings.
ANGULAR_FEATURES.update({"images_panel": False}) ANGULAR_FEATURES.update({"images_panel": False, "flavors_panel": False})

View File

@ -305,6 +305,7 @@ COMPRESS_OFFLINE_CONTEXT = 'horizon.themes.offline_context'
# Dictionary of currently available angular features # Dictionary of currently available angular features
ANGULAR_FEATURES = { ANGULAR_FEATURES = {
'images_panel': True, 'images_panel': True,
'flavors_panel': False,
} }
# Notice all customizable configurations should be above this line # Notice all customizable configurations should be above this line

View File

@ -33,6 +33,7 @@
.module('horizon.app.core', [ .module('horizon.app.core', [
'horizon.app.core.conf', 'horizon.app.core.conf',
'horizon.app.core.cloud-services', 'horizon.app.core.cloud-services',
'horizon.app.core.flavors',
'horizon.app.core.images', 'horizon.app.core.images',
'horizon.app.core.metadata', 'horizon.app.core.metadata',
'horizon.app.core.openstack-service-api', 'horizon.app.core.openstack-service-api',

View File

@ -0,0 +1,121 @@
/**
* (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.flavors
*
* @description
* Provides all of the services and widgets required
* to support and display flavors related content.
*/
angular
.module('horizon.app.core.flavors', [
'ngRoute',
'horizon.framework.conf',
'horizon.app.core'
])
.constant('horizon.app.core.flavors.resourceType', 'OS::Nova::Flavor')
.run(run)
.config(config);
run.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.flavors.basePath',
'horizon.app.core.flavors.service',
'horizon.app.core.flavors.resourceType'
];
function run(registry, basePath, flavorsService, flavorResourceType) {
registry.getResourceType(flavorResourceType)
.setNames(gettext('Flavor'), gettext('Flavors'))
.setSummaryTemplateUrl(basePath + 'summary.html')
.setProperties(flavorProperties())
.setListFunction(flavorsService.getFlavorsPromise)
.tableColumns
.append({
id: 'name',
priority: 1
})
.append({
id: 'vcpus',
priority: 2
})
.append({
id: 'ram',
priority: 1,
sortDefault: true
})
.append({
id: 'disk',
priority: 2
})
.append({
id: 'id',
priority: 1
})
.append({
id: 'os-flavor-access:is_public',
priority: 2
});
/**
* @name roleProperties
* @description resource properties for flavor module
*/
function flavorProperties() {
return {
name: gettext('Flavor Name'),
vcpus: gettext('VCPUs'),
ram: {label: gettext('RAM'), filters: ['mb']},
disk: {label: gettext('Root Disk'), filters: ['gb']},
'OS-FLV-EXT-DATA:ephemeral': {label: gettext('Ephmeral Disk'), filters: ['gb']},
swap: {label: gettext('Swap Disk'), filters: ['gb']},
rxtx_factor: gettext('RX/TX Factor'),
id: gettext('ID'),
'os-flavor-access:is_public': {label: gettext('Public'), filters: ['yesno']},
metadata: gettext('Metadata')
};
}
}
config.$inject = [
'$provide',
'$windowProvider',
'$routeProvider'
];
/**
* @name config
* @param {Object} $provide
* @param {Object} $windowProvider
* @param {Object} $routeProvider
* @description Routes used by this module.
* @returns {undefined} Returns nothing
*/
function config($provide, $windowProvider, $routeProvider) {
var path = $windowProvider.$get().STATIC_URL + 'app/core/flavors/';
$provide.constant('horizon.app.core.flavors.basePath', path);
$routeProvider.when('/admin/flavors', {
templateUrl: path + 'panel.html'
});
}
})();

View File

@ -0,0 +1,37 @@
/*
* (c) Copyright 2016 Hewlett Packard Enterprise Development LP
*
* 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.flavors')
.factory('horizon.app.core.flavors.service', flavorsService);
flavorsService.$inject = [
'horizon.app.core.openstack-service-api.nova'
];
function flavorsService(nova) {
return {
getFlavorsPromise: getFlavorsPromise
};
function getFlavorsPromise(params) {
var fullParams = angular.extend({}, params, {get_extras: true});
return nova.getFlavors(fullParams);
}
}
})();

View File

@ -0,0 +1,41 @@
/*
* (c) Copyright 2016 Hewlett Packard Enterprise Development LP
*
* 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('flavors service', function() {
var service;
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.app.core.flavors'));
beforeEach(inject(function($injector) {
service = $injector.get('horizon.app.core.flavors.service');
}));
describe('getFlavorsPromise', function() {
it("provides a promise that gets translated", inject(function($q, $injector) {
var glance = $injector.get('horizon.app.core.openstack-service-api.nova');
var deferred = $q.defer();
spyOn(glance, 'getFlavors').and.returnValue(deferred.promise);
service.getFlavorsPromise({});
deferred.resolve({data: {items: [{id: 1, updated_at: 'jul1'}]}});
expect(glance.getFlavors).toHaveBeenCalled();
}));
});
});
})();

View File

@ -0,0 +1,3 @@
<hz-resource-panel resource-type-name="OS::Nova::Flavor">
<hz-resource-table resource-type-name="OS::Nova::Flavor"></hz-resource-table>
</hz-resource-panel>

View File

@ -0,0 +1,19 @@
<hz-resource-property-list
resource-type-name="OS::Nova::Flavor"
item="item"
property-groups="[
['name', 'id'],
['vcpus', 'ram'],
['disk', 'OS-FLV-EXT-DATA:ephemeral', 'swap'],
['rxtx_factor']]">
</hz-resource-property-list>
<div class="row">
<div class="col-md-4">
<dl>
<div ng-repeat="(key, value) in item.extras">
<dt>{$ key $}</dt>
<dd>{$ value $}</dd>
</div>
</dl>
</div>
</div>

View File

@ -468,14 +468,8 @@
* call per flavor). * call per flavor).
* @returns {Object} The result of the API call * @returns {Object} The result of the API call
*/ */
function getFlavors(isPublic, getExtras) { function getFlavors(params) {
var config = {'params': {}}; var config = params ? { 'params' : params} : { 'params' : {} };
if (isPublic) {
config.params.is_public = 'true';
}
if (getExtras) {
config.params.get_extras = 'true';
}
return apiService.get('/api/nova/flavors/', config) return apiService.get('/api/nova/flavors/', config)
.success(function (data) { .success(function (data) {
// The colon character ':' in the flavor data causes problems when used // The colon character ':' in the flavor data causes problems when used

View File

@ -286,8 +286,6 @@
}, },
"error": "Unable to retrieve the flavors.", "error": "Unable to retrieve the flavors.",
"testInput": [ "testInput": [
false,
false
] ]
}, },
{ {
@ -301,8 +299,7 @@
}, },
"error": "Unable to retrieve the flavors.", "error": "Unable to retrieve the flavors.",
"testInput": [ "testInput": [
true, {is_public: "true"}
false
] ]
}, },
{ {
@ -316,8 +313,7 @@
}, },
"error": "Unable to retrieve the flavors.", "error": "Unable to retrieve the flavors.",
"testInput": [ "testInput": [
false, {get_extras: "true"}
true
] ]
}, },
{ {
@ -332,8 +328,7 @@
}, },
"error": "Unable to retrieve the flavors.", "error": "Unable to retrieve the flavors.",
"testInput": [ "testInput": [
true, {is_public: "true", get_extras: "true"}
true
] ]
}, },
{ {

View File

@ -102,6 +102,12 @@ SeleniumGroup = [
help="Is the browser size maximized for each test?"), help="Is the browser size maximized for each test?"),
] ]
FlavorsGroup = [
cfg.StrOpt('panel_type',
default='legacy',
help='type/version of flavors panel'),
]
ScenarioGroup = [ ScenarioGroup = [
cfg.StrOpt('ssh_user', cfg.StrOpt('ssh_user',
default='cirros', default='cirros',
@ -163,6 +169,7 @@ def get_config():
cfg.CONF.register_opts(NetworkGroup, group="network") cfg.CONF.register_opts(NetworkGroup, group="network")
cfg.CONF.register_opts(AvailableServiceGroup, group="service_available") cfg.CONF.register_opts(AvailableServiceGroup, group="service_available")
cfg.CONF.register_opts(SeleniumGroup, group="selenium") cfg.CONF.register_opts(SeleniumGroup, group="selenium")
cfg.CONF.register_opts(FlavorsGroup, group="flavors")
cfg.CONF.register_opts(ImageGroup, group="image") cfg.CONF.register_opts(ImageGroup, group="image")
cfg.CONF.register_opts(ScenarioGroup, group="scenario") cfg.CONF.register_opts(ScenarioGroup, group="scenario")
cfg.CONF.register_opts(InstancesGroup, group="launch_instances") cfg.CONF.register_opts(InstancesGroup, group="launch_instances")

View File

@ -33,6 +33,9 @@ explicit_wait=90
# (boolean) # (boolean)
maximize_browser=yes maximize_browser=yes
[flavors]
panel_type=legacy
[image] [image]
# http accessible image (string value) # http accessible image (string value)
panel_type=angular panel_type=angular

View File

@ -15,6 +15,8 @@ from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import menus from openstack_dashboard.test.integration_tests.regions import menus
from openstack_dashboard.test.integration_tests.regions import tables from openstack_dashboard.test.integration_tests.regions import tables
from selenium.webdriver.common import by
class FlavorsTable(tables.TableRegion): class FlavorsTable(tables.TableRegion):
name = "flavors" name = "flavors"
@ -144,3 +146,12 @@ class FlavorsPage(basepage.BaseNavigationPage):
def is_flavor_public(self, name): def is_flavor_public(self, name):
row = self._get_flavor_row(name) row = self._get_flavor_row(name)
return row.cells[self.FLAVORS_TABLE_PUBLIC_COLUMN].text == "Yes" return row.cells[self.FLAVORS_TABLE_PUBLIC_COLUMN].text == "Yes"
class FlavorsPageNG(FlavorsPage):
_resource_page_header_locator = (by.By.CSS_SELECTOR,
'hz-resource-panel hz-page-header h1')
@property
def header(self):
return self._get_element(*self._resource_page_header_locator)

View File

@ -12,10 +12,28 @@
import random import random
from openstack_dashboard.test.integration_tests import decorators
from openstack_dashboard.test.integration_tests import helpers from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages from openstack_dashboard.test.integration_tests.regions import messages
@decorators.config_option_required('flavors.panel_type', 'angular',
message="Legacy Panels not tested")
class TestFlavorAngular(helpers.AdminTestCase):
@property
def flavors_page(self):
from openstack_dashboard.test.integration_tests.pages.admin.\
system.flavorspage import FlavorsPageNG
self.home_pg.go_to_system_flavorspage()
return FlavorsPageNG(self.driver, self.CONFIG)
def test_basic_flavors_browse(self):
flavors_page = self.flavors_page
self.assertEqual(flavors_page.header.text, 'Flavors')
@decorators.config_option_required('flavors.panel_type', 'legacy',
message="Angular Panels not tested")
class TestFlavors(helpers.AdminTestCase): class TestFlavors(helpers.AdminTestCase):
FLAVOR_NAME = helpers.gen_random_resource_name("flavor") FLAVOR_NAME = helpers.gen_random_resource_name("flavor")
@ -48,6 +66,18 @@ class TestFlavors(helpers.AdminTestCase):
self.assertFalse( self.assertFalse(
self.flavors_page.is_flavor_present(self.FLAVOR_NAME)) self.flavors_page.is_flavor_present(self.FLAVOR_NAME))
def test_flavor_header(self):
header_text = self.driver.find_element_by_tag_name('h1').text
self.assertEqual(header_text, 'Flavors')
def test_flavor_module_exists(self):
js_cmd = "$('html').append('<div id=\"testonly\">'"\
" + angular.module('horizon.app.core.flavors').name"\
" + '</div>');"
self.driver.execute_script(js_cmd)
value = self.driver.find_element_by_id('testonly').text
self.assertEqual(value, 'horizon.app.core.flavors')
def test_flavor_create(self): def test_flavor_create(self):
"""tests the flavor creation and deletion functionalities: """tests the flavor creation and deletion functionalities:
* creates a new flavor * creates a new flavor

View File

@ -99,7 +99,8 @@ HORIZON_CONFIG = {
} }
ANGULAR_FEATURES = { ANGULAR_FEATURES = {
'images_panel': False # Use the legacy panel so unit tests are still run 'images_panel': False, # Use the legacy panel so unit tests are still run
'flavors_panel': False,
} }
STATICFILES_DIRS = settings_utils.get_xstatic_dirs( STATICFILES_DIRS = settings_utils.get_xstatic_dirs(

View File

@ -0,0 +1,13 @@
---
prelude: >
The Flavor panel now may be configured to use
either the legacy or Angular code.
features:
- ANGULAR_FEATURES now allows for a key 'flavors_panel' to be
specified as True or False indicating whether the Angular
version of the panel is enabled.
- Integration tests for Flavor features may also be toggled
in openstack_dashboard/test/integration_tests/horizon.conf
using the 'panel_type' feature in the 'flavors' setting,
either set to 'legacy' or 'angular' to match the enabled
panel type.