Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve dashboard title #228

Merged
merged 11 commits into from
Dec 19, 2023
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))

huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
<!--
### 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))

-->
1 change: 1 addition & 0 deletions vizro-core/examples/default/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ def create_home_page():


dashboard = vm.Dashboard(
title="Vizro demo",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably just left this in for demo purposes, but I would remove it if this is close to being merged. It's not optimal in terms of design if there are controls/navigation on the page, as we still have this open discussion of where this might go.

Looking at this, I prefer the upper header container we discussed the other day even more 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I left it here so that we would hopefully apply the same change to vizro.mckinsey.com next time we update the demo, but let me remove it if we're not yet happy about the visual design.

The reason I think it would be nice to include sooner rather than later is because every time someone shares a link to vizro.mckinsey.com the social media preview will have a slightly strange label (like just "Homepage") - it would be nicer if it instead said "Vizro demo: Homepage" or similar, to make it clear what the link being shared actually is.

If we don't work out the page layout/visual design soon then possible alternatives might be:

  • we remove the title from the dashboard layout altogether from _make_page_layout for now
  • we remove the title from the dashboard layout just by customising our demo app

So I'll revert the change here and leave it up to you to decide out what's the best solution here depending on how long the reworking of the page layout takes 🙂 It's not a huge thing but I don't think we should wait too long (say > 1 month) to get an improved shareable link to our demo app just because we're blocked on where to put the title on screen.

Copy link
Contributor

@huong-li-nguyen huong-li-nguyen Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we remove the title from the dashboard layout altogether from _make_page_layout for now
I think this would be a breaking change right? Or are we still fine saying that any visual changes are not considered breaking changes? 😄 I would be fine with this one to be honest - if people reach out, then we at least know people have been using a dashboard title and need it 😅

we remove the title from the dashboard layout just by customising our demo app
As a quick-fix, this one seems safer. The demo app would be a bit out of sync for a while with the example app here, but I think it's fine as we manage it.

What do you prefer? 1) I could embed in my current PR on layout arrangements and 2) I can create in another PR in the demo-app repository

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in 5cef34a.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't consider removing the title from the layout a breaking change since it's just something visual and no one's code will break in any way. Admittedly it's kind of a drastic one rather than just tweaking styling, but I'm completely fine with it and, like you say, if people ask where their title went then it's a good signal they want it there.

Which solution we go for depends to me on how close we are to resolving the question of where the title goes and how upsetting people find the current placement:

  1. if we're really not happy with how the dashboard title appears at the moment and it feels a long way off resolving let's go for option 1
  2. if we think it's ok currently but still don't want it on our demo app then let's go for 2
  3. if we think the current dashboard title is already completely fine then let's just leave it as it is and add a title to the example app

Personally I'm completely fine with the current visual appearance, which is why I just went for option 3 to begin with in this PR. But you have a better idea of how upsetting people with more design sensibilities find that and where future changes to layout might take us here, so completely up to you which option to choose 🙂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense!

I think the position of the dashboard title will likely change given that it's taking too much horizontal space if there are no controls/navigations. Given that J. is also out, I'll go for 2) as I think it's the one he would feel less reluctant to implement 😄

I add it to my tickets and will update the demo accordingly after we've done release 0.1.8 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thank you! Sounds good to me.

I didn't actually realise how weird it looks like when there's no controls/navigation until I just looked again now. You're right that doesn't look good at all, so I take back my statement above that option 3 was ok by me. Just in case you thought I had no design sensibilities at all 😀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. hopefully the demo update would be as simple as dash.layout["dashboard_title_outer"].hidden = True or similar rather than needing any customisation of _make_page_layout with a custom Dashboard model.

pages=[
create_home_page(),
create_variable_analysis(),
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one line is the only functional change in the PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if people do want to overwrite it, they would overwrite the pre-build of the model here?

I was actually wondering how this works compared to the dash.app.title. If you set the dash.app.title, I noticed that it appears for a second and then it gets overwritten by anything defined in dash.register_page.

This might be worth mentioning in the docs if people try to overwrite it via the dash.app.title. This wouldn't actually work, because in the end it gets overwritten by the code here again. But I think this is also how it works in a pure Dash app?

app = Vizro(assets_folder="../assets").build(dashboard)
app.title = "My title"
app.run()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed how it works in a pure Dash app also:

  1. Dash(title="X") corresponds to the property dash.app.title and sets the initial value of <title>X</title>
  2. but this only appears very briefly when you're using Dash pages, because it gets overwritten straight away by dash.page_registry[page.id]["title"]
  3. after this, the original app title=X has no effect on anything

So for Vizro users where Dash pages is always used, the title argument basically has no effect. @AnnMarieW please do correct me if I've missed something here.

We have possibly added to the confusion here because we have our own Dashboard.title property, and it's not obvious to a user what effect that has on anything. I considered before whether we should do dash.app.title = dashboard.title in Vizro.build() but decided it was basically pointless because, like you say, it just gets overwritten straight away. I think it would be a very small improvement though, since that way you wouldn't see the "Dash" title at all, so let me add it in here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change made in c00fe4e.

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()

antonymilne marked this conversation as resolved.
Show resolved Hide resolved

@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())
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

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)
antonymilne marked this conversation as resolved.
Show resolved Hide resolved
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.
)
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

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
Loading