diff options
10 files changed, 303 insertions, 23 deletions
diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py index 11dcfbb1..38fe859d 100644 --- a/tuskar_ui/api.py +++ b/tuskar_ui/api.py @@ -211,7 +211,10 @@ class Flavor(object): def list_deployed_ids(cls, request): """Get and memoize ID's of deployed flavors.""" servers = nova.server_list(request)[0] - return set(server.flavor['id'] for server in servers) + deployed_ids = set(server.flavor['id'] for server in servers) + roles = OvercloudRole.list(request) + deployed_ids |= set(role.flavor_id for role in roles) + return deployed_ids class Overcloud(base.APIResourceWrapper): @@ -866,12 +869,17 @@ class Node(base.APIResourceWrapper): """ task_state = self._apiresource.task_state task_state_dict = { + 'initializing': 'initializing', 'active': 'on', 'reboot': 'rebooting', 'building': 'building', 'deploying': 'deploying', 'prepared': 'prepared', 'deleting': 'deleting', + 'deploy failed': 'deploy failed', + 'deploy complete': 'deploy complete', + 'deleted': 'deleted', + 'error': 'error', } return task_state_dict.get(task_state, 'off') diff --git a/tuskar_ui/infrastructure/flavors/tests.py b/tuskar_ui/infrastructure/flavors/tests.py index 1b43d125..5145e27c 100644 --- a/tuskar_ui/infrastructure/flavors/tests.py +++ b/tuskar_ui/infrastructure/flavors/tests.py @@ -63,18 +63,20 @@ def _prepare_create(): class FlavorsTest(test.BaseAdminViewTests): def test_index(self): + roles = TEST_DATA.tuskarclient_overcloud_roles.list() with contextlib.nested( patch('openstack_dashboard.api.nova.flavor_list', return_value=TEST_DATA.novaclient_flavors.list()), patch('openstack_dashboard.api.nova.server_list', return_value=([], False)), - ) as (flavors_mock, servers_mock): + patch('tuskar_ui.api.OvercloudRole.list', return_value=roles), + ) as (flavors_mock, servers_mock, role_list_mock): res = self.client.get(INDEX_URL) self.assertEqual(flavors_mock.call_count, 1) self.assertEqual(servers_mock.call_count, 1) + self.assertEqual(role_list_mock.call_count, 1) - self.assertTemplateUsed(res, - 'infrastructure/flavors/index.html') + self.assertTemplateUsed(res, 'infrastructure/flavors/index.html') def test_index_recoverable_failure(self): with patch('openstack_dashboard.api.nova.flavor_list', @@ -88,8 +90,7 @@ class FlavorsTest(test.BaseAdminViewTests): return_value=([], False)) as mock: res = self.client.get(CREATE_URL) self.assertEqual(mock.call_count, 2) - self.assertTemplateUsed(res, - 'infrastructure/flavors/create.html') + self.assertTemplateUsed(res, 'infrastructure/flavors/create.html') def test_create_get_recoverable_failure(self): with patch('openstack_dashboard.api.glance.image_list_detailed', @@ -125,18 +126,20 @@ class FlavorsTest(test.BaseAdminViewTests): patch('openstack_dashboard.api.nova.flavor_delete'), patch('openstack_dashboard.api.nova.server_list', return_value=([], False)), + patch('tuskar_ui.api.OvercloudRole.list', return_value=[]), patch('openstack_dashboard.api.glance.image_list_detailed', return_value=([], False)), patch('openstack_dashboard.api.nova.flavor_list', return_value=TEST_DATA.novaclient_flavors.list()) - ) as (delete_mock, server_list_mock, glance_mock, flavors_mock): + ) as (delete_mock, server_list_mock, _role_list_mock, _glance_mock, + _flavors_mock): res = self.client.post(INDEX_URL, data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) self.assertEqual(delete_mock.call_count, 2) self.assertEqual(server_list_mock.call_count, 1) - def test_delete_deployed(self): + def test_delete_deployed_on_servers(self): flavors = TEST_DATA.novaclient_flavors.list() server = servers.Server( servers.ServerManager(None), @@ -152,11 +155,37 @@ class FlavorsTest(test.BaseAdminViewTests): patch('openstack_dashboard.api.nova.flavor_delete'), patch('openstack_dashboard.api.nova.server_list', return_value=([server], False)), + patch('tuskar_ui.api.OvercloudRole.list', return_value=[]), + patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=([], False)), + patch('openstack_dashboard.api.nova.flavor_list', + return_value=TEST_DATA.novaclient_flavors.list()) + ) as (delete_mock, server_list_mock, _role_list_mock, _glance_mock, + _flavors_mock): + res = self.client.post(INDEX_URL, data) + self.assertMessageCount(error=1, warning=0) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertEqual(delete_mock.call_count, 1) + self.assertEqual(server_list_mock.call_count, 1) + + def test_delete_deployed_on_roles(self): + flavors = TEST_DATA.novaclient_flavors.list() + roles = TEST_DATA.tuskarclient_roles_with_flavors.list() + + data = {'action': 'flavors__delete', + 'object_ids': [flavors[0].id, flavors[1].id]} + with contextlib.nested( + patch('openstack_dashboard.api.nova.flavor_delete'), + patch('openstack_dashboard.api.nova.server_list', + return_value=([], False)), + patch('tuskar_ui.api.OvercloudRole.list', return_value=roles), patch('openstack_dashboard.api.glance.image_list_detailed', return_value=([], False)), patch('openstack_dashboard.api.nova.flavor_list', return_value=TEST_DATA.novaclient_flavors.list()) - ) as (delete_mock, server_list_mock, glance_mock, flavors_mock): + ) as (delete_mock, server_list_mock, _role_list_mock, _glance_mock, + _flavors_mock): res = self.client.post(INDEX_URL, data) self.assertMessageCount(error=1, warning=0) self.assertNoFormErrors(res) diff --git a/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html b/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html index 090fcc73..32ae2010 100644 --- a/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html +++ b/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html @@ -2,22 +2,22 @@ id="tab-{{ form.prefix }}"> <div class="form form-inline"> <div class="row-fluid"> - <h3 style="margin-bottom:16px">Node Detail</h3> + <h4>Node Detail</h4> {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.node_tags %} </div> <div class="row-fluid"> - <h4>Power Management</h4> + <h5>Power Management</h5> {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_address %} {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_user %} {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_password %} </div> <div class="row-fluid"> - <h4>Networking</h4> + <h5>Networking</h5> {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.mac_address required=True %} </div> <div class="row-fluid"> <div class="span4"> - <h4>Hardware</h4> + <h5>Hardware</h5> </div> <label class="span6 checkbox checkbox-inline"> {{ form.introspect_hardware }}<small> {{ form.introspect_hardware.label }}</small> diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html index 03c7c43a..b9f247f2 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html @@ -174,7 +174,7 @@ <div class="span4"> <div class="widget"> <h2>{% trans "Deployment Role Distribution" %}</h2> - <div class="d3_pie_chart_distribution pull-left" data-used='{% for role in roles %}{{ role.name }}={{ role.deployed_node_count|default:"0" }}{% if not forloop.last %}|{% endif %}{% endfor %}'></div> + <div class="d3_pie_chart_distribution" data-used='{% for role in roles %}{{ role.name }}={{ role.deployed_node_count|default:"0" }}{% if not forloop.last %}|{% endif %}{% endfor %}'></div> </div> <div class="clear"></div> <div class="widget"> diff --git a/tuskar_ui/infrastructure/static/infrastructure/less/horizon_upgrades.less b/tuskar_ui/infrastructure/static/infrastructure/less/horizon_upgrades.less new file mode 100644 index 00000000..11c3513f --- /dev/null +++ b/tuskar_ui/infrastructure/static/infrastructure/less/horizon_upgrades.less @@ -0,0 +1,223 @@ +// general +h2 { + font-weight: 200; + font-size: 24px; + color: rgb(80, 80, 80); + line-height: 130%; +} + +h3 { + font-weight: lighter; + font-size: 20px; + color: rgb(80,80,80); + margin-bottom: 3px; + margin: 0 0 3px 0; +} + +h4 { + font-weight: lighter; + font-size: 18px; + color: rgb(120,120,120); + margin-bottom: 3px; +} + +h5 { + font-weight: lighter; + font-size: 16px; + color: rgb(120,120,120); + margin-bottom: 3px; +} + +// topbar +.topbar { + background: rgb(239, 239, 239); + padding: 0.5em 2em; + font-size: 90%; + height: 24px; + border-bottom: 1px solid rgb(220, 220, 220); +} + +h1.brand a { + height: 24px; + width: 107px; + margin: 0 1em 0 0; + background-size: auto 24px; +} + +.topbar .switcher_bar h3 { + line-height: 18px; + font-size: 11px !important; +} + +#tenant_switcher { + display: none; +} + +#user_info { + margin: 0; + padding: 0; + line-height: 24px; + font-size: 11px !important; + + & > a { + font-size: 11px !important; + } +} + +// sidebar +.sidebar { + margin: 2.5em 2em; + border: 0 none; + background: none; +} + +.nav_accordion { + background: none; + + // dashboards + dt { + font-size: 90%; + color: rgb(170, 170, 170); + background-color: transparent; + border: 0 none; + border-bottom: 3px solid rgb(230, 230, 230); + padding: 0.2em 2em 0.2em 0; + + &:not(:first-child) { + margin-top: 1em; + } + + &.active { + color: rgb(120, 120, 120); + border-bottom: 3px solid rgb(130, 130, 130); + } + + div { + font-size: inherit; + color: inherit; + font-weight: normal; + margin: 0; + } + } + + // panel groups + dd { + margin: 0; + + h4 { + margin: 0; + border: 1px solid rgb(210, 210, 210); + background-color: rgb(240, 240, 240); + color: rgb(140, 140, 140); + max-width: none; + + div { + margin: 0 0 0 14px; + } + + &.active { + border: 1px solid rgb(210, 210, 210); + background-color: rgb(230, 230, 230); + color: rgb(90, 90, 90); + } + } + + // items + ul { + width: auto; + margin: 0 0 0 14px; + + li a { + width: auto; + margin: 0; + border-bottom: 1px solid rgb(190, 190, 190); + border-left: 5px solid rgb(230, 230, 230); + color: rgb(90, 90, 90); + opacity: 0.8; + + &:last-child { + margin: 0; + } + + &.active { + color: rgb(215, 65, 50); + border-top: 0 none; + border-left: 5px solid rgb(215, 65, 50); + background-color: rgb(245, 245, 245); + margin: 0; + border-radius: 0; + opacity: 1; + } + + &:hover { + background-color: rgb(245, 245, 245); + opacity: 1; + } + } + } + } +} + +// content body +#content_body { + padding-left: 300px; + padding-right: 35px; + padding-top: 2em; + padding-bottom: 3.5em; +} + +.page-header { + margin-bottom: 0; + padding: 0; +} + +// content nav +#main_content { + .nav-tabs { + margin: 0 0 30px; + border: 0 none; + + li a { + font-weight: 500; + position: relative; + top: -1px; + padding: 10px 15px; + border-radius: 0 0 4px 4px; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + border-top-width: 0; + border-color: transparent; + color: #43A1D6; + } + + li.active a { + border: 1px solid rgb(220, 220, 220); + background: #43A1D6; + color: white; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + + &:after { + color: inherit; + } + } + + li:not(.active) a:hover { + border: 1px solid rgb(240, 240, 240); + border-top-width: 0; + background: rgb(244, 244, 244); + } + } + + .tab-content { + padding: 0; + border: 0 none; + } + + // content area + #content_body > .row-fluid { + margin: 0; + } + + .widget:not(:last-child) { + margin: 0 0 2em 0; + } +} diff --git a/tuskar_ui/infrastructure/static/infrastructure/less/individual_pages.less b/tuskar_ui/infrastructure/static/infrastructure/less/individual_pages.less index 9f49a670..9bc000e0 100644 --- a/tuskar_ui/infrastructure/static/infrastructure/less/individual_pages.less +++ b/tuskar_ui/infrastructure/static/infrastructure/less/individual_pages.less @@ -18,26 +18,31 @@ .register-nodes-formset { a.add-node-link { display: block; - margin-top: 6px; } + .nav-tabs > .active > a { color: @white; background-color: @linkColor; } + ul.nav-tabs > li span.delete-icon { display: none; } + ul.nav-tabs > li.active span.delete-icon { display: block; margin: 4px 19px 4px 0; cursor: pointer; } + ul.nav-tabs > li { position: relative; } + ul.nav-tabs > li.active { width: 107%; } + ul.nav-tabs > li.active:after { display: block; content: ''; @@ -48,12 +53,16 @@ border-bottom: 16px solid transparent; border-left: 8px solid @linkColor; } + .register-nav-head { margin-top: 19px; + margin-bottom: 5px; } - .form h4, .form h3 { - margin-bottom: 16px; + + .form h5 { + margin: 0.5em 0 0.75em 0; } + .form label.checkbox { font-weight: normal; } diff --git a/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less b/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less index bd5ed043..0bbf3def 100644 --- a/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less +++ b/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less @@ -10,6 +10,7 @@ @import "index_pages.less"; @import "individual_pages.less"; @import "tables.less"; +@import "horizon_upgrades.less"; // disable text select on element .unselectable { @@ -21,10 +22,9 @@ } .actions { - margin: 0 0 @baseLineHeight / 3 0; -} -.fullscreen-workflow-body { - overflow: auto; + // not nice, but actions should be placed above the line + // will fix the style for now, better solution needed + margin: -33px 0 0 0; } // hide the project switcher from the top bar diff --git a/tuskar_ui/infrastructure/templates/formset_table/menu_formset.html b/tuskar_ui/infrastructure/templates/formset_table/menu_formset.html index 755805cf..325ae6aa 100644 --- a/tuskar_ui/infrastructure/templates/formset_table/menu_formset.html +++ b/tuskar_ui/infrastructure/templates/formset_table/menu_formset.html @@ -8,7 +8,7 @@ <div class="clearfix register-nav-head"> <a class="pull-right add-node-link" href="#"><i class="icon-plus"></i> {% trans "Add Node" %}</a> - <h3>Nodes to register</h3> + <h4>Nodes to register</h4> </div> <ul class="nav nav-tabs nav-stacked"> {% for form in formset %} diff --git a/tuskar_ui/infrastructure/templates/infrastructure/_fullscreen_workflow.html b/tuskar_ui/infrastructure/templates/infrastructure/_fullscreen_workflow.html index 83ac08cf..9ac2025f 100644 --- a/tuskar_ui/infrastructure/templates/infrastructure/_fullscreen_workflow.html +++ b/tuskar_ui/infrastructure/templates/infrastructure/_fullscreen_workflow.html @@ -1,7 +1,7 @@ {% load i18n %} {% with workflow.get_entry_point as entry_point %} <div class="row-fluid"> - <form class="form form-horizontal" {{ workflow.attr_string|safe }} action="{{ workflow.get_absolute_url }}" {% if add_to_field %}data-add-to-field="{{ add_to_field }}"{% endif %} method="POST"{% if workflow.multipart %} enctype="multipart/form-data"{% endif %}> + <form class="form form-horizontal span12" {{ workflow.attr_string|safe }} action="{{ workflow.get_absolute_url }}" {% if add_to_field %}data-add-to-field="{{ add_to_field }}"{% endif %} method="POST"{% if workflow.multipart %} enctype="multipart/form-data"{% endif %}> {% csrf_token %} {% if REDIRECT_URL %}<input type="hidden" name="{{ workflow.redirect_param_name }}" value="{{ REDIRECT_URL }}"/>{% endif %} <div class="fullscreen-workflow-body"> diff --git a/tuskar_ui/test/test_data/tuskar_data.py b/tuskar_ui/test/test_data/tuskar_data.py index ea24b4f2..659b0931 100644 --- a/tuskar_ui/test/test_data/tuskar_data.py +++ b/tuskar_ui/test/test_data/tuskar_data.py @@ -441,3 +441,14 @@ def data(TEST): 'disk': 60}) flavor_2.get_keys = lambda: {'cpu_arch': 'i386'} TEST.novaclient_flavors.add(flavor_1, flavor_2) + + # OvercloudRoles with flavors associated + TEST.tuskarclient_roles_with_flavors = test_data_utils.TestDataContainer() + role_with_flavor = overcloud_roles.OvercloudRole( + overcloud_roles.OvercloudRoleManager(None), + {'id': 5, + 'name': 'Block Storage', + 'description': 'block storage overcloud role', + 'flavor_id': '1', + 'image_name': 'overcloud-block-storage'}) + TEST.tuskarclient_roles_with_flavors.add(role_with_flavor) |