Skip to content

Commit

Permalink
Improve dashboard title (#228)
Browse files Browse the repository at this point in the history
Signed-off-by: Antony Milne <[email protected]>
  • Loading branch information
antonymilne authored Dec 19, 2023
1 parent 5fc8dff commit 2212992
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->

### Added

- When set, the dashboard title appears alongside the individual page title as the text labeling a browser tab. ([#228](https://github.com/mckinsey/vizro/pull/228))

<!--
### Changed
- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#228](https://github.com/mckinsey/vizro/pull/228))
-->
<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Fixed
- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
7 changes: 5 additions & 2 deletions vizro-core/docs/pages/user_guides/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This guide shows you how to configure and call a [`Dashboard`][vizro.models.Dashboard] using either
pydantic models, python dictionaries, yaml or json.

To create a dashboard, do the following steps:
To create a dashboard:

1. Choose one of the possible configuration syntaxes
2. Create your `pages`, see our guide on [Pages](pages.md)
Expand Down Expand Up @@ -191,4 +191,7 @@ After running the dashboard, you can access the dashboard via `localhost:8050`.

## Adding a dashboard title

When providing a `title` to the [`Dashboard`][vizro.models.Dashboard], it will automatically be added as a header for each [`Page`][vizro.models.Page].
If supplied, the `title` of the [`Dashboard`][vizro.models.Dashboard] displays a heading at the top left of every page. It is also combined with the `title` specified in [`Page`][vizro.models.Page] to set:

- `<title>` HTML element that controls the text labeling a browser window;
- `<meta>` elements that control how a preview is generated when sharing a link to your dashboard (e.g. on social media).
3 changes: 3 additions & 0 deletions vizro-core/schemas/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

if args.check:
if json.loads(schema_path.read_text()) != json.loads(schema_json):
# Ideally just doing hatch run:schema would be fine, but the schema slightly depends
# on Python version (Python 3.8 vs. Python 3.11 give different results), even
# for the same pydantic version.
sys.exit("JSON schema is out of date. Run `hatch run all.py3.11:schema` to update it.")
print("JSON schema is up to date.") # noqa: T201
else:
Expand Down
6 changes: 5 additions & 1 deletion vizro-core/src/vizro/_vizro.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, **kwargs):
kwargs: Passed through to `Dash.__init__`, e.g. `assets_folder`, `url_base_pathname`. See
[Dash documentation](https://dash.plotly.com/reference#dash.dash) for possible arguments.
"""
self.dash = dash.Dash(**kwargs, use_pages=True, pages_folder="")
self.dash = dash.Dash(**kwargs, use_pages=True, pages_folder="", title="Vizro")
self.dash.config.external_stylesheets.append(
"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
)
Expand Down Expand Up @@ -61,6 +61,10 @@ def build(self, dashboard: Dashboard):
Returns:
Vizro: App object
"""
# Note Dash.index uses self.dash.title instead of self.dash.app.config.title.
if dashboard.title:
self.dash.title = dashboard.title

# Note that model instantiation and pre_build are independent of Dash.
self._pre_build()

Expand Down
8 changes: 6 additions & 2 deletions vizro-core/src/vizro/models/_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,13 @@ def pre_build(self):
# For now the homepage (path /) corresponds to self.pages[0].
# Note redirect_from=["/"] doesn't work and so the / route must be defined separately.
for order, page in enumerate(self.pages):
path = page.path if order else "/"
dash.register_page(
module=page.id, name=page.title, path=path, order=order, layout=partial(self._make_page_layout, page)
module=page.id,
name=page.title,
title=f"{self.title}: {page.title}" if self.title else page.title,
path=page.path if order else "/",
order=order,
layout=partial(self._make_page_layout, page),
)
dash.register_page(module=MODULE_PAGE_404, layout=self._make_page_404_layout())

Expand Down
8 changes: 0 additions & 8 deletions vizro-core/tests/unit/vizro/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,3 @@ def vizro_app():
app instantiation.pages.
"""
return Vizro()


@pytest.fixture()
def prebuilt_two_page_dashboard(vizro_app, page_1, page_2):
"""Minimal two page dashboard, used mainly for testing navigation."""
dashboard = vm.Dashboard(pages=[page_1, page_2])
dashboard.pre_build()
return dashboard
9 changes: 9 additions & 0 deletions vizro-core/tests/unit/vizro/models/_navigation/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import pytest

import vizro.models as vm


@pytest.fixture()
def pages_as_list():
Expand All @@ -11,3 +13,10 @@ def pages_as_list():
@pytest.fixture
def pages_as_dict():
return {"Group": ["Page 1", "Page 2"]}


@pytest.fixture()
def prebuilt_two_page_dashboard(vizro_app, page_1, page_2):
dashboard = vm.Dashboard(pages=[page_1, page_2])
dashboard.pre_build()
return dashboard
182 changes: 81 additions & 101 deletions vizro-core/tests/unit/vizro/models/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import json
from collections import OrderedDict
from functools import partial

import dash
import dash_bootstrap_components as dbc
import plotly
import pytest
from asserts import assert_component_equal
from dash import html

try:
Expand All @@ -18,88 +17,6 @@
from vizro.actions._action_loop._action_loop import ActionLoop


@pytest.fixture()
def dashboard_container():
return dbc.Container(
id="dashboard_container_outer",
children=[
html.Div(vizro.__version__, id="vizro_version", hidden=True),
ActionLoop._create_app_callbacks(),
dash.page_container,
],
className="vizro_dark",
fluid=True,
)


@pytest.fixture()
def mock_page_registry(prebuilt_two_page_dashboard, page_1, page_2):
return OrderedDict(
{
"Page 1": {
"module": "Page 1",
"supplied_path": "/",
"path_template": None,
"path": "/",
"supplied_name": "Page 1",
"name": "Page 1",
"supplied_title": None,
"title": "Page 1",
"description": "",
"order": 0,
"supplied_order": 0,
"supplied_layout": partial(prebuilt_two_page_dashboard._make_page_layout, page_1),
"supplied_image": None,
"image": None,
"image_url": None,
"redirect_from": None,
"layout": partial(prebuilt_two_page_dashboard._make_page_layout, page_1),
"relative_path": "/",
},
"Page 2": {
"module": "Page 2",
"supplied_path": "/page-2",
"path_template": None,
"path": "/page-2",
"supplied_name": "Page 2",
"name": "Page 2",
"supplied_title": None,
"title": "Page 2",
"description": "",
"order": 1,
"supplied_order": 1,
"supplied_layout": partial(prebuilt_two_page_dashboard._make_page_layout, page_2),
"supplied_image": None,
"image": None,
"image_url": None,
"redirect_from": None,
"layout": partial(prebuilt_two_page_dashboard._make_page_layout, page_2),
"relative_path": "/page-2",
},
"not_found_404": {
"module": "not_found_404",
"supplied_path": None,
"path_template": None,
"path": "/not-found-404",
"supplied_name": None,
"name": "Not found 404",
"supplied_title": None,
"title": "Not found 404",
"description": "",
"order": None,
"supplied_order": None,
"supplied_layout": prebuilt_two_page_dashboard._make_page_404_layout(),
"supplied_image": None,
"image": None,
"image_url": None,
"redirect_from": None,
"layout": prebuilt_two_page_dashboard._make_page_404_layout(),
"relative_path": "/not-found-404",
},
}
)


class TestDashboardInstantiation:
"""Tests model instantiation and the validators run at that time."""

Expand Down Expand Up @@ -149,27 +66,90 @@ def test_field_invalid_theme_input_type(self, page_1):
class TestDashboardPreBuild:
"""Tests dashboard pre_build method."""

def test_dashboard_page_registry(self, prebuilt_two_page_dashboard, mock_page_registry):
result = dash.page_registry
expected = mock_page_registry
# Str conversion required as comparison of OrderedDict values result in False otherwise
assert str(result.items()) == str(expected.items())

def test_create_layout_page_404(self, prebuilt_two_page_dashboard, mocker):
mocker.patch("vizro.models._dashboard.get_relative_path")
result = prebuilt_two_page_dashboard._make_page_404_layout()
result_image = result.children[0]
result_div = result.children[1]

assert isinstance(result, html.Div)
assert isinstance(result_image, html.Img)
assert isinstance(result_div, html.Div)
def test_page_registry(self, vizro_app, page_1, page_2, mocker):
mock_register_page = mocker.patch("dash.register_page", autospec=True)
mock_make_page_404_layout = mocker.patch(
"vizro.models._dashboard.Dashboard._make_page_404_layout"
) # Checking the actual dash components is done in test_make_page_404_layout.
vm.Dashboard(pages=[page_1, page_2]).pre_build()

mock_register_page.assert_any_call(
module=page_1.id,
name="Page 1",
title="Page 1",
path="/",
order=0,
layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
)
mock_register_page.assert_any_call(
module=page_2.id,
name="Page 2",
title="Page 2",
path="/page-2",
order=1,
layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
)
mock_register_page.assert_any_call(
module="not_found_404",
layout=mock_make_page_404_layout(),
)
assert mock_register_page.call_count == 3

def test_page_registry_with_title(self, vizro_app, page_1, mocker):
mock_register_page = mocker.patch("dash.register_page", autospec=True)
vm.Dashboard(pages=[page_1], title="My dashboard").pre_build()

mock_register_page.assert_any_call(
module=page_1.id,
name="Page 1",
title="My dashboard: Page 1",
path="/",
order=0,
layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
)

def test_make_page_404_layout(self, vizro_app):
# vizro_app fixture is needed to avoid mocking out get_relative_path.
expected = html.Div(
[
html.Img(src="/vizro/images/errors/error_404.svg"),
html.Div(
[
html.Div(
[
html.H3("This page could not be found.", className="heading-3-600"),
html.P("Make sure the URL you entered is correct."),
],
className="error_text_container",
),
dbc.Button("Take me home", href="/", className="button_primary"),
],
className="error_content_container",
),
],
className="page_error_container",
)

assert_component_equal(vm.Dashboard._make_page_404_layout(), expected, {})


class TestDashboardBuild:
"""Tests dashboard build method."""

def test_dashboard_build(self, dashboard_container, prebuilt_two_page_dashboard):
result = json.loads(json.dumps(prebuilt_two_page_dashboard.build(), cls=plotly.utils.PlotlyJSONEncoder))
def test_dashboard_build(self, vizro_app, page_1, page_2):
dashboard = vm.Dashboard(pages=[page_1, page_2])
dashboard.pre_build()

dashboard_container = dbc.Container(
id="dashboard_container_outer",
children=[
html.Div(vizro.__version__, id="vizro_version", hidden=True),
ActionLoop._create_app_callbacks(),
dash.page_container,
],
className="vizro_dark",
fluid=True,
)
result = json.loads(json.dumps(dashboard.build(), cls=plotly.utils.PlotlyJSONEncoder))
expected = json.loads(json.dumps(dashboard_container, cls=plotly.utils.PlotlyJSONEncoder))
assert result == expected

0 comments on commit 2212992

Please sign in to comment.