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

Move layout creation to Dashboard #142

Merged
merged 13 commits into from
Nov 1, 2023
5 changes: 2 additions & 3 deletions .github/workflows/lint-vizro-ai.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ jobs:
- name: Find added changelog fragments
id: added-files
run: |
pwd
if ${{ github.event_name == 'pull_request' }}; then
echo "added_files=$(git diff --name-only --diff-filter=A -r HEAD^1 HEAD -- changelog.d/*.md | xargs)" >> $GITHUB_OUTPUT
else
Expand All @@ -64,9 +63,9 @@ jobs:
run: |
if [ -z "${{ steps.added-files.outputs.added_files }}" ];
then
echo "No changelog fragment .md file within changelog.d was detected. Run 'hatch run docs:changelog' to create such a fragment.";
echo "No changelog fragment .md file within changelog.d was detected. Run 'hatch run changelog:add' to create such a fragment.";
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
echo "If your PR contains changes that should be mentioned in the CHANGELOG in the next release, please uncomment the relevant section in your created fragment and describe the changes to the user."
echo "If your changes are not relevant for the CHANGELOG, please save and commit the file as."
echo "If your changes are not relevant for the CHANGELOG, please save and commit the file as is."
exit 1
else
echo "${{ steps.added-files.outputs.added_files }} was added - ready to go!";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
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

- A bullet item for the Added 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))

-->
<!--
### 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 ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### 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))

-->
45 changes: 38 additions & 7 deletions vizro-core/src/vizro/models/_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, List, Literal, Optional
from functools import partial
from typing import TYPE_CHECKING, List, Literal, Optional, cast

import dash
import dash_bootstrap_components as dbc
import dash_daq as daq
import plotly.io as pio
from dash import ClientsideFunction, Input, Output, clientside_callback, html
from pydantic import Field, validator
Expand Down Expand Up @@ -94,8 +96,10 @@ def pre_build(self):
# 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=page.build)
self._create_error_page_404()
dash.register_page(
module=page.id, name=page.title, path=path, order=order, layout=partial(self._make_page_layout, page)
)
dash.register_page(module=MODULE_PAGE_404, layout=create_layout_page_404())
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

@_log_call
def build(self):
Expand All @@ -114,14 +118,41 @@ def build(self):
fluid=True,
)

def _make_page_layout(self, page):
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
# Identical across pages
dashboard_title = (
html.Div(children=[html.H2(self.title), html.Hr()], className="dashboard_title", id="dashboard_title_outer")
if self.title
else html.Div(className="hidden", id="dashboard_title_outer")
)
theme_switch = daq.BooleanSwitch(
id="theme_selector", on=True if self.theme == "vizro_dark" else False, persistence=True
)

# Shared across pages but slightly differ in content
page_title = html.H2(children=page.title, id="page_title")
navigation = cast(Navigation, self.navigation).build(active_page_id=page.id)

# Different across pages
page_content = page.build()
control_panel = page_content["control_panel_outer"]
component_container = page_content["component_container_outer"]
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

# Arrangement
header = html.Div(children=[page_title, theme_switch], className="header", id="header_outer")
left_side_elements = [dashboard_title, navigation, control_panel]
left_side = (
html.Div(children=left_side_elements, className="left_side", id="left_side_outer")
if any(left_side_elements)
else html.Div(className="hidden", id="left_side_outer")
)
right_side = html.Div(children=[header, component_container], className="right_side", id="right_side_outer")
return html.Div([left_side, right_side], className="page_container", id="page_container_outer")

@staticmethod
def _update_theme():
clientside_callback(
ClientsideFunction(namespace="clientside", function_name="update_dashboard_theme"),
Output("dashboard_container_outer", "className"),
Input("theme_selector", "on"),
)

@staticmethod
def _create_error_page_404():
return dash.register_page(module=MODULE_PAGE_404, layout=create_layout_page_404())
111 changes: 24 additions & 87 deletions vizro-core/src/vizro/models/_page.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
from __future__ import annotations

from typing import List, Optional, cast
from typing import List, Optional

import dash_bootstrap_components as dbc
import dash_daq as daq
from dash import Input, Output, Patch, callback, dcc, html
from pydantic import Field, root_validator, validator

import vizro._themes as themes
from vizro._constants import ON_PAGE_LOAD_ACTION_PREFIX
from vizro.actions import _on_page_load
from vizro.managers import model_manager
from vizro.managers._model_manager import DuplicateIDError
from vizro.models import Action, Dashboard, Graph, Layout, Navigation, VizroBaseModel
from vizro.models import Action, Graph, Layout, VizroBaseModel
from vizro.models._action._actions_chain import ActionsChain, Trigger
from vizro.models._models_utils import _log_call, get_unique_grid_component_ids

Expand Down Expand Up @@ -118,6 +115,11 @@ def pre_build(self):
def build(self):
self._update_graph_theme()
controls_content = [control.build() for control in self.controls]
control_panel = (
html.Div(children=[*controls_content, html.Hr()], className="control_panel", id="control_panel_outer")
if controls_content
else html.Div(className="hidden", id="control_panel_outer")
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
)
components_content = [
html.Div(
component.build(),
Expand All @@ -130,7 +132,8 @@ def build(self):
self.components, self.layout.component_grid_lines # type: ignore[union-attr]
)
]
return self._make_page_layout(controls_content, components_content)
components_container = self._create_component_container(components_content)
return html.Div([control_panel, components_container])

def _update_graph_theme(self):
outputs = [
Expand All @@ -150,90 +153,24 @@ def update_graph_theme(theme_selector_on: bool):
patched_figure["layout"]["template"] = themes.dark if theme_selector_on else themes.light
return [patched_figure] * len(outputs)

@staticmethod
def _create_theme_switch():
_, dashboard = next(model_manager._items_with_type(Dashboard))
theme_switch = daq.BooleanSwitch(
id="theme_selector", on=True if dashboard.theme == "vizro_dark" else False, persistence=True
)
return theme_switch

@staticmethod
def _create_control_panel(controls_content):
control_panel = html.Div(
children=[*controls_content, html.Hr()], className="control_panel", id="control_panel_outer"
)
return control_panel if controls_content else None

def _create_nav_panel(self):
_, dashboard = next(model_manager._items_with_type(Dashboard))
return cast(Navigation, dashboard.navigation).build(active_page_id=self.id)

def _create_component_container(self, components_content):
component_container = html.Div(
children=html.Div(
components_content,
style={
"gridRowGap": self.layout.row_gap, # type: ignore[union-attr]
"gridColumnGap": self.layout.col_gap, # type: ignore[union-attr]
"gridTemplateColumns": f"repeat({len(self.layout.grid[0])}," # type: ignore[union-attr]
f"minmax({self.layout.col_min_width}, 1fr))",
"gridTemplateRows": f"repeat({len(self.layout.grid)}," # type: ignore[union-attr]
f"minmax({self.layout.row_min_height}, 1fr))",
},
className="component_container_grid",
),
children=[
html.Div(
components_content,
style={
"gridRowGap": self.layout.row_gap, # type: ignore[union-attr]
"gridColumnGap": self.layout.col_gap, # type: ignore[union-attr]
"gridTemplateColumns": f"repeat({len(self.layout.grid[0])}," # type: ignore[union-attr]
f"minmax({self.layout.col_min_width}, 1fr))",
"gridTemplateRows": f"repeat({len(self.layout.grid)}," # type: ignore[union-attr]
f"minmax({self.layout.row_min_height}, 1fr))",
},
className="component_container_grid",
),
dcc.Store(id=f"{ON_PAGE_LOAD_ACTION_PREFIX}_trigger_{self.id}"),
],
className="component_container",
id="component_container_outer",
)
return component_container

@staticmethod
def _arrange_containers(page_title, theme_switch, nav_panel, control_panel, component_container):
"""Defines div container arrangement on page.

To change arrangement, one has to change the order in the header, left_side and/or right_side_elements.
"""
_, dashboard = next(model_manager._items_with_type(Dashboard))
dashboard_title = (
html.Div(
children=[html.H2(dashboard.title), html.Hr()], className="dashboard_title", id="dashboard_title_outer"
)
if dashboard.title
else None
)

header_elements = [page_title, theme_switch]
left_side_elements = [dashboard_title, nav_panel, control_panel]
header = html.Div(children=header_elements, className="header", id="header_outer")
left_side = (
html.Div(children=left_side_elements, className="left_side", id="left_side_outer")
if any(left_side_elements)
else None
)
right_side_elements = [header, component_container]
right_side = html.Div(children=right_side_elements, className="right_side", id="right_side_outer")
return left_side, right_side

def _make_page_layout(self, controls_content, components_content):
# Create dashboard containers/elements
page_title = html.H2(children=self.title)
theme_switch = self._create_theme_switch()
nav_panel = self._create_nav_panel()
control_panel = self._create_control_panel(controls_content)
component_container = self._create_component_container(components_content)

# Arrange dashboard containers
left_side, right_side = self._arrange_containers(
page_title=page_title,
theme_switch=theme_switch,
nav_panel=nav_panel,
control_panel=control_panel,
component_container=component_container,
)

return dbc.Container(
id=self.id,
children=[dcc.Store(id=f"{ON_PAGE_LOAD_ACTION_PREFIX}_trigger_{self.id}"), left_side, right_side],
className="page_container",
)
11 changes: 6 additions & 5 deletions vizro-core/tests/unit/vizro/models/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from collections import OrderedDict
from functools import partial

import dash
import dash_bootstrap_components as dbc
Expand Down Expand Up @@ -29,7 +30,7 @@ def dashboard_container():


@pytest.fixture()
def mock_page_registry(page1, page2):
def mock_page_registry(dashboard, page1, page2):
return OrderedDict(
{
"Page 1": {
Expand All @@ -44,12 +45,12 @@ def mock_page_registry(page1, page2):
"description": "",
"order": 0,
"supplied_order": 0,
"supplied_layout": page1.build,
"supplied_layout": partial(dashboard._make_page_layout, page1),
"supplied_image": None,
"image": None,
"image_url": None,
"redirect_from": None,
"layout": page1.build,
"layout": partial(dashboard._make_page_layout, page1),
"relative_path": "/",
},
"Page 2": {
Expand All @@ -64,12 +65,12 @@ def mock_page_registry(page1, page2):
"description": "",
"order": 1,
"supplied_order": 1,
"supplied_layout": page2.build,
"supplied_layout": partial(dashboard._make_page_layout, page2),
"supplied_image": None,
"image": None,
"image_url": None,
"redirect_from": None,
"layout": page2.build,
"layout": partial(dashboard._make_page_layout, page2),
"relative_path": "/page-2",
},
"not_found_404": {
Expand Down
Loading