Make 'switch' between legacy and Angular Images
This patch follows on the example that the Containers set, providing a 'switch' in the panel-enablement file that currently defaults to 'legacy' (Python-based Images panel) and allows for 'angular' (Angular- based Images panel). To be clear, this does NOT enable Angular Images. It's just setting the stage to do so at some point, or to allow deployers/devs to easily switch between the two. A switch both for HORIZON_CONFIG and for integration tests is necessary due to the way integration tests operate. Co-Authored-By: Timur Sufiev <tsufiev@mirantis.com> Change-Id: I12cd33552218ed1082d2d9a2ae8982639a217a6a Partially-Implements: blueprint angularize-images-table
This commit is contained in:
parent
de1a23267a
commit
20bc6e1516
@ -180,6 +180,18 @@ A dictionary containing classes of exceptions which Horizon's centralized
|
||||
exception handling should be aware of. Based on these exception categories,
|
||||
Horizon will handle the exception and display a message to the user.
|
||||
|
||||
``images_panel``
|
||||
-----------
|
||||
|
||||
.. versionadded:: 10.0.0(Newton)
|
||||
|
||||
Default: ``legacy``
|
||||
|
||||
There are currently two panel types that may be specified: ``legacy`` and
|
||||
``angular``. ``legacy`` will display the Python-based (server-side) Images
|
||||
panel and ``angular`` will display the Angular-based (client-side) Images
|
||||
panel.
|
||||
|
||||
``modal_backdrop``
|
||||
------------------
|
||||
|
||||
|
@ -16,16 +16,24 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.admin.images import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<image_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<image_id>[^/]+)/detail/$',
|
||||
views.DetailView.as_view(), name='detail')
|
||||
]
|
||||
if settings.HORIZON_CONFIG['images_panel'] == 'angular':
|
||||
# New angular images
|
||||
urlpatterns = [
|
||||
url(r'^$', views.AngularIndexView.as_view(), name='index'),
|
||||
url(r'^(?P<image_id>[^/]+)/detail/$',
|
||||
views.AngularIndexView.as_view(), name='detail'),
|
||||
]
|
||||
else:
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<image_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<image_id>[^/]+)/detail/$',
|
||||
views.DetailView.as_view(), name='detail')
|
||||
]
|
||||
|
@ -23,6 +23,7 @@ from oslo_utils import units
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views import generic
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
@ -40,6 +41,10 @@ from openstack_dashboard.dashboards.admin.images \
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AngularIndexView(generic.TemplateView):
|
||||
template_name = 'angular.html'
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
DEFAULT_FILTERS = {'is_public': None}
|
||||
table_class = project_tables.AdminImagesTable
|
||||
|
@ -16,14 +16,23 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.project.images.images import views
|
||||
from openstack_dashboard.dashboards.project.images import views as imgviews
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<image_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<image_id>[^/]+)/$', views.DetailView.as_view(), name='detail'),
|
||||
]
|
||||
if settings.HORIZON_CONFIG['images_panel'] == 'angular':
|
||||
urlpatterns = [
|
||||
url(r'^(?P<image_id>[^/]+)/$', imgviews.AngularIndexView.as_view(),
|
||||
name='detail'),
|
||||
]
|
||||
else:
|
||||
urlpatterns = [
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<image_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<image_id>[^/]+)/$', views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
]
|
||||
|
@ -16,6 +16,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.conf.urls import url
|
||||
|
||||
@ -26,8 +27,16 @@ from openstack_dashboard.dashboards.project.images.snapshots \
|
||||
from openstack_dashboard.dashboards.project.images import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'', include(image_urls, namespace='images')),
|
||||
url(r'', include(snapshot_urls, namespace='snapshots')),
|
||||
]
|
||||
if settings.HORIZON_CONFIG['images_panel'] == 'angular':
|
||||
# New angular images
|
||||
urlpatterns = [
|
||||
url(r'^$', views.AngularIndexView.as_view(), name='index'),
|
||||
url(r'', include(image_urls, namespace='images')),
|
||||
url(r'', include(snapshot_urls, namespace='snapshots')),
|
||||
]
|
||||
else:
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'', include(image_urls, namespace='images')),
|
||||
url(r'', include(snapshot_urls, namespace='snapshots')),
|
||||
]
|
||||
|
@ -22,6 +22,7 @@ Views for managing Images and Snapshots.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views import generic
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
@ -34,6 +35,10 @@ from openstack_dashboard.dashboards.project.images.images \
|
||||
import tables as images_tables
|
||||
|
||||
|
||||
class AngularIndexView(generic.TemplateView):
|
||||
template_name = 'angular.html'
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
table_class = images_tables.ImagesTable
|
||||
template_name = 'project/images/index.html'
|
||||
|
@ -1276,11 +1276,6 @@ class InstanceTests(helpers.TestCase):
|
||||
server.id,
|
||||
"snapshot1").AndReturn(self.snapshots.first())
|
||||
|
||||
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||
marker=None,
|
||||
paginate=True) \
|
||||
.AndReturn([[], False, False])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'instance_id': server.id,
|
||||
|
@ -1,23 +0,0 @@
|
||||
# (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.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class NGImages(horizon.Panel):
|
||||
name = _("Images")
|
||||
slug = 'ngimages'
|
||||
permissions = ('openstack.services.image',)
|
@ -1,22 +0,0 @@
|
||||
# (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.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.project.ngimages import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url('', views.IndexView.as_view(), name='index'),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# (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.
|
||||
|
||||
from django.views import generic
|
||||
|
||||
|
||||
class IndexView(generic.TemplateView):
|
||||
template_name = 'angular.html'
|
@ -1,3 +1,17 @@
|
||||
# (c) Copyright 2016 Hewlett Packard Enterprise Development Company 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.
|
||||
PANEL = 'images'
|
||||
# The slug of the dashboard the PANEL associated with. Required.
|
||||
|
@ -1,30 +0,0 @@
|
||||
# (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.
|
||||
|
||||
# The slug of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'project'
|
||||
|
||||
# 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 = 'compute'
|
||||
|
||||
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'ngimages'
|
||||
|
||||
# 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.project.ngimages.panel.NGImages'
|
@ -79,6 +79,7 @@ HORIZON_CONFIG = {
|
||||
'js_spec_files': [],
|
||||
'external_templates': [],
|
||||
'plugins': [],
|
||||
'images_panel': 'legacy',
|
||||
'integration_tests_support': INTEGRATION_TESTS_SUPPORT
|
||||
}
|
||||
|
||||
|
@ -295,9 +295,25 @@
|
||||
var path = $windowProvider.$get().STATIC_URL + 'app/core/images/';
|
||||
$provide.constant('horizon.app.core.images.basePath', path);
|
||||
|
||||
$routeProvider.when('/project/ngimages/', {
|
||||
$routeProvider.when('/project/images/:id/', {
|
||||
redirectTo: goToAngularDetails
|
||||
});
|
||||
|
||||
$routeProvider.when('/admin/images/:id/detail/', {
|
||||
redirectTo: goToAngularDetails
|
||||
});
|
||||
|
||||
$routeProvider.when('/project/images/', {
|
||||
templateUrl: path + 'panel.html'
|
||||
});
|
||||
|
||||
$routeProvider.when('/admin/images/', {
|
||||
templateUrl: path + 'panel.html'
|
||||
});
|
||||
|
||||
function goToAngularDetails(params) {
|
||||
return 'project/ngdetails/OS::Glance::Image/' + params.id;
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -57,6 +57,9 @@ IdentityGroup = [
|
||||
]
|
||||
|
||||
ImageGroup = [
|
||||
cfg.StrOpt('panel_type',
|
||||
default='legacy',
|
||||
help='type/version of images panel'),
|
||||
cfg.StrOpt('http_image',
|
||||
default='http://download.cirros-cloud.net/0.3.1/'
|
||||
'cirros-0.3.1-x86_64-uec.tar.gz',
|
||||
|
@ -58,6 +58,18 @@ NOT_TEST_OBJECT_ERROR_MSG = "Decorator can be applied only on test" \
|
||||
" classes and test methods."
|
||||
|
||||
|
||||
def _get_skip_method(obj):
|
||||
"""Make sure that we can decorate both methods and classes."""
|
||||
if inspect.isclass(obj):
|
||||
if not _is_test_cls(obj):
|
||||
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
|
||||
return _mark_class_skipped
|
||||
else:
|
||||
if not _is_test_method_name(obj.__name__):
|
||||
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
|
||||
return _mark_method_skipped
|
||||
|
||||
|
||||
def services_required(*req_services):
|
||||
"""Decorator for marking test's service requirements,
|
||||
if requirements are not met in the configuration file
|
||||
@ -85,15 +97,7 @@ def services_required(*req_services):
|
||||
.
|
||||
"""
|
||||
def actual_decoration(obj):
|
||||
# make sure that we can decorate method and classes as well
|
||||
if inspect.isclass(obj):
|
||||
if not _is_test_cls(obj):
|
||||
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
|
||||
skip_method = _mark_class_skipped
|
||||
else:
|
||||
if not _is_test_method_name(obj.__name__):
|
||||
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
|
||||
skip_method = _mark_method_skipped
|
||||
skip_method = _get_skip_method(obj)
|
||||
# get available services from configuration
|
||||
avail_services = config.get_config().service_available
|
||||
for req_service in req_services:
|
||||
@ -105,6 +109,32 @@ def services_required(*req_services):
|
||||
return actual_decoration
|
||||
|
||||
|
||||
def _parse_compound_config_option_value(option_name):
|
||||
"""Parses the value of a given config option where option's section name is
|
||||
separated from option name by '.'.
|
||||
"""
|
||||
name_parts = option_name.split('.')
|
||||
name_parts.reverse()
|
||||
option = config.get_config()
|
||||
while name_parts:
|
||||
option = getattr(option, name_parts.pop())
|
||||
return option
|
||||
|
||||
|
||||
def config_option_required(option_key, required_value, message=None):
|
||||
if message is None:
|
||||
message = "%s option equal to '%s' is required for this test to work" \
|
||||
" properly." % (option_key, required_value)
|
||||
|
||||
def actual_decoration(obj):
|
||||
skip_method = _get_skip_method(obj)
|
||||
option_value = _parse_compound_config_option_value(option_key)
|
||||
if option_value != required_value:
|
||||
obj = skip_method(obj, message)
|
||||
return obj
|
||||
return actual_decoration
|
||||
|
||||
|
||||
def skip_because(**kwargs):
|
||||
"""Decorator for skipping tests hitting known bugs
|
||||
|
||||
@ -120,14 +150,7 @@ def skip_because(**kwargs):
|
||||
.
|
||||
"""
|
||||
def actual_decoration(obj):
|
||||
if inspect.isclass(obj):
|
||||
if not _is_test_cls(obj):
|
||||
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
|
||||
skip_method = _mark_class_skipped
|
||||
else:
|
||||
if not _is_test_method_name(obj.__name__):
|
||||
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
|
||||
skip_method = _mark_method_skipped
|
||||
skip_method = _get_skip_method(obj)
|
||||
bugs = kwargs.get("bugs")
|
||||
if bugs and isinstance(bugs, collections.Iterable):
|
||||
for bug in bugs:
|
||||
|
@ -35,6 +35,7 @@ maximize_browser=yes
|
||||
|
||||
[image]
|
||||
# http accessible image (string value)
|
||||
panel_type=legacy
|
||||
http_image=http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
|
||||
images_list=cirros-0.3.4-x86_64-uec,
|
||||
cirros-0.3.4-x86_64-uec-kernel,
|
||||
|
@ -9,6 +9,8 @@
|
||||
# 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 selenium.webdriver.common import by
|
||||
|
||||
from openstack_dashboard.test.integration_tests.pages import basepage
|
||||
from openstack_dashboard.test.integration_tests.regions import forms
|
||||
from openstack_dashboard.test.integration_tests.regions import tables
|
||||
@ -250,3 +252,12 @@ class ImagesPage(basepage.BaseNavigationPage):
|
||||
launch_instance.count.value = instance_count
|
||||
launch_instance.submit()
|
||||
return InstancesPage(self.driver, self.conf)
|
||||
|
||||
|
||||
class ImagesPageNG(ImagesPage):
|
||||
_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)
|
||||
|
@ -15,14 +15,39 @@ from openstack_dashboard.test.integration_tests import helpers
|
||||
from openstack_dashboard.test.integration_tests.regions import messages
|
||||
|
||||
|
||||
class TestImagesBasic(helpers.TestCase):
|
||||
"""Login as demo user"""
|
||||
IMAGE_NAME = helpers.gen_random_resource_name("image")
|
||||
|
||||
@decorators.config_option_required('image.panel_type', 'legacy',
|
||||
message="Angular Panels not tested")
|
||||
class TestImagesLegacy(helpers.TestCase):
|
||||
@property
|
||||
def images_page(self):
|
||||
return self.home_pg.go_to_compute_imagespage()
|
||||
|
||||
|
||||
@decorators.config_option_required('image.panel_type', 'angular',
|
||||
message="Legacy Panels not tested")
|
||||
class TestImagesAngular(helpers.TestCase):
|
||||
@property
|
||||
def images_page(self):
|
||||
# FIXME(tsufiev): had to return angularized version of Images Page
|
||||
# object with the horrendous hack below because it's not so easy to
|
||||
# wire into the Navigation machinery and tell it to return an '*NG'
|
||||
# version of ImagesPage class if one adds '_ng' suffix to
|
||||
# 'go_to_compute_imagespage()' method. Yet that's how it should work
|
||||
# (or rewrite Navigation module completely).
|
||||
from openstack_dashboard.test.integration_tests.pages.project.\
|
||||
compute.imagespage import ImagesPageNG
|
||||
self.home_pg.go_to_compute_imagespage()
|
||||
return ImagesPageNG(self.driver, self.CONFIG)
|
||||
|
||||
def test_basic_image_browse(self):
|
||||
images_page = self.images_page
|
||||
self.assertEqual(images_page.header.text, 'Images')
|
||||
|
||||
|
||||
class TestImagesBasic(TestImagesLegacy):
|
||||
"""Login as demo user"""
|
||||
IMAGE_NAME = helpers.gen_random_resource_name("image")
|
||||
|
||||
def image_create(self, local_file=None):
|
||||
images_page = self.images_page
|
||||
if local_file:
|
||||
@ -227,14 +252,10 @@ class TestImagesBasic(helpers.TestCase):
|
||||
self.image_delete(new_image_name)
|
||||
|
||||
|
||||
class TestImagesAdvanced(helpers.TestCase):
|
||||
class TestImagesAdvanced(TestImagesLegacy):
|
||||
"""Login as demo user"""
|
||||
IMAGE_NAME = helpers.gen_random_resource_name("image")
|
||||
|
||||
@property
|
||||
def images_page(self):
|
||||
return self.home_pg.go_to_compute_imagespage()
|
||||
|
||||
def test_create_volume_from_image(self):
|
||||
"""This test case checks create volume from image functionality:
|
||||
Steps:
|
||||
@ -293,7 +314,7 @@ class TestImagesAdvanced(helpers.TestCase):
|
||||
self.assertTrue(instances_page.is_instance_deleted(target_instance))
|
||||
|
||||
|
||||
class TestImagesAdmin(helpers.AdminTestCase, TestImagesBasic):
|
||||
class TestImagesAdmin(helpers.AdminTestCase, TestImagesLegacy):
|
||||
"""Login as admin user"""
|
||||
IMAGE_NAME = helpers.gen_random_resource_name("image")
|
||||
|
||||
|
@ -96,6 +96,7 @@ HORIZON_CONFIG = {
|
||||
'unauthorized': exceptions.UNAUTHORIZED},
|
||||
'angular_modules': [],
|
||||
'js_files': [],
|
||||
'images_panel': 'legacy',
|
||||
}
|
||||
|
||||
# Load the pluggable dashboard settings
|
||||
|
12
releasenotes/notes/image-panel-switch-38e9d3716451f9e3.yaml
Normal file
12
releasenotes/notes/image-panel-switch-38e9d3716451f9e3.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
prelude: >
|
||||
The Image panel now may be configured to use
|
||||
either the legacy or Angular code.
|
||||
features:
|
||||
- HORIZON_CONFIG now allows for a key 'images_panel' to be
|
||||
specified as 'legacy' or 'angular' indicating the type of
|
||||
panel to use.
|
||||
- Integration tests for Image features may also be toggled
|
||||
in openstack_dashboard/test/integration_tests/horizon.conf
|
||||
using the 'panel_type' feature, either set to 'legacy' or
|
||||
'angular' to match the enabled panel type.
|
Loading…
x
Reference in New Issue
Block a user