diff --git a/vizro-core/changelog.d/20231221_052426_amward_update_meta_tags.md b/vizro-core/changelog.d/20231221_052426_amward_update_meta_tags.md new file mode 100644 index 000000000..a567c184f --- /dev/null +++ b/vizro-core/changelog.d/20231221_052426_amward_update_meta_tags.md @@ -0,0 +1,47 @@ + + + + + +### Added + +- Enable adding description and image to the meta tags. ([#185](https://github.com/mckinsey/vizro/pull/185)) + + + + + diff --git a/vizro-core/examples/assets/images/app.svg b/vizro-core/examples/assets/images/app.svg new file mode 100644 index 000000000..9d07d6372 --- /dev/null +++ b/vizro-core/examples/assets/images/app.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/vizro-core/examples/assets/images/logo.svg b/vizro-core/examples/assets/images/logo.svg new file mode 100644 index 000000000..0904b87de --- /dev/null +++ b/vizro-core/examples/assets/images/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py index 4957f1e00..ca1df8a2e 100644 --- a/vizro-core/examples/default/app.py +++ b/vizro-core/examples/default/app.py @@ -24,6 +24,7 @@ def create_variable_analysis(): """Function returns a page with gapminder data to do variable analysis.""" page_variable = vm.Page( title="Variable Analysis", + description="Analyzing population, GDP per capita and life expectancy on country and continent level", layout=vm.Layout( grid=[ # fmt: off @@ -194,6 +195,7 @@ def create_relation_analysis(): """Function returns a page to perform relation analysis.""" page_relation_analysis = vm.Page( title="Relationship Analysis", + description="Investigating the interconnection between population, GDP per capita and life expectancy", layout=vm.Layout( grid=[[0, 0, 0, 0, 1]] + [[2, 2, 3, 3, 3]] * 4 + [[4, 4, 4, 4, 4]] * 5, row_min_height="100px", @@ -323,6 +325,7 @@ def create_continent_summary(): """Function returns a page with markdown including images.""" page_summary = vm.Page( title="Continent Summary", + description="Summarizing the main findings for each continent", layout=vm.Layout(grid=[[i] for i in range(5)], row_min_height="190px", row_gap="25px"), components=[ vm.Card( @@ -415,6 +418,7 @@ def create_benchmark_analysis(): page_country = vm.Page( title="Benchmark Analysis", + description="Discovering how the metrics differ for each country and export data for further investigation", layout=vm.Layout(grid=[[0, 1]] * 5 + [[2, -1]], col_gap="32px", row_gap="60px"), components=[ vm.Table( @@ -496,6 +500,7 @@ def create_home_page(): """Function returns the homepage.""" page_home = vm.Page( title="Homepage", + description="Vizro demo app for studying gapminder data", layout=vm.Layout(grid=[[0, 1], [2, 3]], row_gap="16px", col_gap="24px"), components=[ vm.Card( diff --git a/vizro-core/schemas/0.1.8.dev0.json b/vizro-core/schemas/0.1.8.dev0.json index 66b0af594..3e1bab3d3 100644 --- a/vizro-core/schemas/0.1.8.dev0.json +++ b/vizro-core/schemas/0.1.8.dev0.json @@ -845,7 +845,7 @@ }, "Page": { "title": "Page", - "description": "A page in [`Dashboard`][vizro.models.Dashboard] with its own URL path and place in the `Navigation`.\n\nArgs:\n components (List[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component\n has to be provided.\n title (str): Title to be displayed.\n layout (Layout): Layout to place components in. Defaults to `None`.\n controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`.\n path (str): Path to navigate to page. Defaults to `\"\"`.\n\nRaises:\n ValueError: If number of page and grid components is not the same", + "description": "A page in [`Dashboard`][vizro.models.Dashboard] with its own URL path and place in the `Navigation`.\n\nArgs:\n components (List[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component\n has to be provided.\n title (str): Title to be displayed.\n description (str): Description for meta tags.\n layout (Layout): Layout to place components in. Defaults to `None`.\n controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`.\n path (str): Path to navigate to page. Defaults to `\"\"`.\n\nRaises:\n ValueError: If number of page and grid components is not the same", "type": "object", "properties": { "id": { @@ -888,6 +888,12 @@ "description": "Title to be displayed.", "type": "string" }, + "description": { + "title": "Description", + "description": "Description for meta tags.", + "default": "", + "type": "string" + }, "layout": { "$ref": "#/definitions/Layout" }, diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 93ac22bed..a5c3736ef 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -2,6 +2,7 @@ import logging from functools import partial +from pathlib import Path from typing import TYPE_CHECKING, List, Literal, TypedDict import dash @@ -88,6 +89,8 @@ def set_navigation_pages(cls, navigation, values): @_log_call def pre_build(self): + meta_image = self._infer_image("app") or self._infer_image("logo") + # Setting order here ensures that the pages in dash.page_registry preserves the order of the List[Page]. # For now the homepage (path /) corresponds to self.pages[0]. # Note redirect_from=["/"] doesn't work and so the / route must be defined separately. @@ -95,6 +98,8 @@ def pre_build(self): dash.register_page( module=page.id, name=page.title, + description=page.description, + image=meta_image, title=f"{self.title}: {page.title}" if self.title else page.title, path=page.path if order else "/", order=order, @@ -193,3 +198,11 @@ def _make_page_404_layout(): ], className="page_error_container", ) + + def _infer_image(self, filename: str): + valid_extensions = [".apng", ".avif", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"] + assets_folder = Path(dash.get_app().config.assets_folder) + if assets_folder.is_dir(): + for path in Path(assets_folder).rglob(f"{filename}.*"): + if path.suffix in valid_extensions: + return str(path.relative_to(assets_folder)) diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py index 17a24d73d..4a24538b5 100644 --- a/vizro-core/src/vizro/models/_page.py +++ b/vizro-core/src/vizro/models/_page.py @@ -32,6 +32,7 @@ class Page(VizroBaseModel): components (List[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component has to be provided. title (str): Title to be displayed. + description (str): Description for meta tags. layout (Layout): Layout to place components in. Defaults to `None`. controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`. path (str): Path to navigate to page. Defaults to `""`. @@ -42,6 +43,7 @@ class Page(VizroBaseModel): components: List[ComponentType] title: str = Field(..., description="Title to be displayed.") + description: str = Field("", description="Description for meta tags.") layout: Layout = None # type: ignore[assignment] controls: List[ControlType] = [] path: str = Field("", description="Path to navigate to page.") diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py index e0cb9be91..03d48cfe1 100644 --- a/vizro-core/tests/unit/vizro/models/test_dashboard.py +++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py @@ -1,4 +1,5 @@ import json +from pathlib import Path import dash import dash_bootstrap_components as dbc @@ -14,6 +15,7 @@ import vizro import vizro.models as vm +from vizro import Vizro from vizro.actions._action_loop._action_loop import ActionLoop from vizro.models._dashboard import _all_hidden @@ -77,6 +79,8 @@ def test_page_registry(self, vizro_app, page_1, page_2, mocker): mock_register_page.assert_any_call( module=page_1.id, name="Page 1", + description="", + image=None, title="Page 1", path="/", order=0, @@ -85,6 +89,8 @@ def test_page_registry(self, vizro_app, page_1, page_2, mocker): mock_register_page.assert_any_call( module=page_2.id, name="Page 2", + description="", + image=None, title="Page 2", path="/page-2", order=1, @@ -103,12 +109,71 @@ def test_page_registry_with_title(self, vizro_app, page_1, mocker): mock_register_page.assert_any_call( module=page_1.id, name="Page 1", + description="", + image=None, title="My dashboard: Page 1", path="/", order=0, layout=mocker.ANY, # partial call is tricky to mock out so we ignore it. ) + def test_page_registry_with_description(self, vizro_app, mocker): + mock_register_page = mocker.patch("dash.register_page", autospec=True) + vm.Dashboard( + pages=[vm.Page(title="Page 1", components=[vm.Button()], description="My description")] + ).pre_build() + + mock_register_page.assert_any_call( + module="Page 1", + name="Page 1", + description="My description", + image=None, + title="Page 1", + path="/", + order=0, + layout=mocker.ANY, # partial call is tricky to mock out so we ignore it. + ) + + @pytest.mark.parametrize( + "image_path", ["app.png", "app.svg", "images/app.png", "images/app.svg", "logo.png", "logo.svg"] + ) + def test_page_registry_with_image(self, page_1, mocker, tmp_path, image_path): + if Path(image_path).parent != Path("."): + Path(tmp_path / image_path).parent.mkdir() + Path(tmp_path / image_path).touch() + Vizro(assets_folder=tmp_path) + mock_register_page = mocker.patch("dash.register_page", autospec=True) + vm.Dashboard(pages=[page_1]).pre_build() + + mock_register_page.assert_any_call( + module=page_1.id, + name="Page 1", + description="", + image=image_path, + title="Page 1", + path="/", + order=0, + layout=mocker.ANY, # partial call is tricky to mock out so we ignore it. + ) + + def test_page_registry_with_images(self, page_1, mocker, tmp_path): + Path(tmp_path / "app.svg").touch() + Path(tmp_path / "logo.svg").touch() + Vizro(assets_folder=tmp_path) + mock_register_page = mocker.patch("dash.register_page", autospec=True) + vm.Dashboard(pages=[page_1]).pre_build() + + mock_register_page.assert_any_call( + module=page_1.id, + name="Page 1", + description="", + image="app.svg", + title="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(