Skip to content

Commit e88a8d8

Browse files
author
Andrei Neagu
committed
refactor
1 parent 77ca02e commit e88a8d8

File tree

4 files changed

+137
-85
lines changed

4 files changed

+137
-85
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from abc import ABC, abstractmethod
2+
from functools import cached_property
3+
4+
from nicegui import ui
5+
from nicegui.element import Element
6+
7+
8+
class ParentMixin:
9+
def __init__(self) -> None:
10+
self._parent: Element | None = None
11+
12+
@staticmethod
13+
def _get_parent() -> Element:
14+
"""overwrite to use a different parent element"""
15+
return ui.element()
16+
17+
@cached_property
18+
def parent(self) -> Element:
19+
if self._parent is None:
20+
self._parent = self._get_parent()
21+
return self._parent
22+
23+
def remove_parent(self) -> None:
24+
if self._parent is not None:
25+
self._parent.delete()
26+
self._parent = None
27+
28+
29+
class DisplayaMixin(ABC):
30+
@abstractmethod
31+
def display(self) -> None:
32+
"""creates ui elements inside the parent container"""
Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
from functools import cached_property
21
from typing import Generic, TypeAlias, TypeVar
32

4-
from nicegui import ui
5-
from nicegui.element import Element
6-
3+
from ._mixins import DisplayaMixin, ParentMixin
74
from .base_display_model import BaseUpdatableDisplayModel
85
from .updatable_component import BaseUpdatableComponent
96

@@ -12,35 +9,13 @@
129
Reference: TypeAlias = str
1310

1411

15-
class SomeRenderer(Generic[M]):
12+
class UpdatableComponentList(DisplayaMixin, ParentMixin, Generic[M]):
1613
def __init__(self, component: type[BaseUpdatableComponent]) -> None:
14+
super().__init__()
1715
self.component = component
1816

1917
self._added_models: dict[Reference, M] = {}
20-
self._rendered_models: dict[Reference, BaseUpdatableComponent] = (
21-
{}
22-
) # TODO: might only need a set here
23-
24-
self._parent: Element | None = None
25-
26-
@cached_property
27-
def parent(self) -> Element:
28-
if self._parent is None:
29-
self._parent = ui.element() # this is an empty div as a parent
30-
return self._parent
31-
32-
def add_or_update_model(self, reference: Reference, model: M) -> None:
33-
if reference not in self._added_models:
34-
self._added_models[reference] = model
35-
self._render_component(reference)
36-
else:
37-
self._added_models[reference].update(model)
38-
39-
def remove_model(self, reference: Reference) -> None:
40-
if reference in self._added_models:
41-
self._added_models[reference].remove_from_ui()
42-
del self._added_models[reference]
43-
del self._rendered_models[reference]
18+
self._rendered_models: set[Reference] = set()
4419

4520
def _render_to_parent(self) -> None:
4621
with self.parent:
@@ -53,7 +28,33 @@ def _render_component(self, reference: Reference) -> None:
5328
model = self._added_models[reference]
5429
component = self.component(model)
5530
component.display()
56-
self._rendered_models[reference] = component
31+
self._rendered_models.add(reference)
5732

5833
def display(self) -> None:
5934
self._render_to_parent()
35+
36+
def add_or_update_model(self, reference: Reference, display_model: M) -> None:
37+
"""adds or updates and existing ui element form a given model"""
38+
if reference not in self._added_models:
39+
self._added_models[reference] = display_model
40+
self._render_component(reference)
41+
else:
42+
self._added_models[reference].update(display_model)
43+
44+
def remove_model(self, reference: Reference) -> None:
45+
"""removes a model from the ui via it's given reference"""
46+
if reference in self._added_models:
47+
self._added_models[reference].remove_from_ui()
48+
del self._added_models[reference]
49+
self._rendered_models.remove(reference)
50+
51+
def update_from_dict(self, models: dict[Reference, M]) -> None:
52+
"""updates UI given a new input"""
53+
# remove models that are not in the new list
54+
for reference in tuple(self._added_models.keys()):
55+
if reference not in models:
56+
self.remove_model(reference)
57+
58+
# add or update existing models
59+
for reference, model in models.items():
60+
self.add_or_update_model(reference, model)

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_common/updatable_component.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
1-
from abc import ABC, abstractmethod
1+
from abc import abstractmethod
22
from typing import Generic, TypeVar
33

4-
from nicegui import ui
5-
from nicegui.element import Element
6-
4+
from ._mixins import DisplayaMixin, ParentMixin
75
from .base_display_model import BaseUpdatableDisplayModel
86

9-
T = TypeVar("T", bound=BaseUpdatableDisplayModel)
10-
7+
M = TypeVar("M", bound=BaseUpdatableDisplayModel)
118

12-
class BaseUpdatableComponent(ABC, Generic[T]):
13-
def __init__(self, display_model: T):
14-
self.display_model = display_model
159

16-
self._parent: Element | None = None
17-
self.display_model.on_remove_from_ui(self._remove_from_ui)
10+
class BaseUpdatableComponent(DisplayaMixin, ParentMixin, Generic[M]):
11+
def __init__(self, display_model: M):
12+
super().__init__()
1813

19-
def _remove_from_ui(self) -> None:
20-
if self._parent is not None:
21-
self._parent.delete()
22-
self._parent = None
14+
self.display_model = display_model
15+
self.display_model.on_remove_from_ui(self.remove_parent)
2316

2417
def display(self) -> None:
25-
if self._parent is None:
26-
self._parent = ui.element() # this is an empty div as a parent
27-
with self._parent:
18+
with self.parent:
2819
self._draw_ui()
2920

3021
@abstractmethod

services/dynamic-scheduler/tests/unit/api_frontend/_common/test_updatable_component.py

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
BaseUpdatableDisplayModel,
1818
)
1919
from simcore_service_dynamic_scheduler.api.frontend._common.some_renderer import (
20-
SomeRenderer,
20+
UpdatableComponentList,
2121
)
2222
from simcore_service_dynamic_scheduler.api.frontend._common.updatable_component import (
2323
BaseUpdatableComponent,
@@ -159,41 +159,42 @@ def _draw_ui(self) -> None:
159159

160160
class PersonComponent(BaseUpdatableComponent[Person]):
161161
def _draw_ui(self) -> None:
162-
# NOTE:
163-
# There are 3 ways to bind the UI to the model changes:
164-
# 1. using nicegui builting facilties
165-
# 2. via model attribute VALE change
166-
# 3. via model attribute TYPE change
167-
# The model attribute changes allow to trigger re-rendering of subcomponents.
168-
# This should be mainly used for chainging the UI layout based on
169-
# the attribute's value or type.
170-
171-
# 1. bind the label directly to the model's attribute
172-
ui.label().bind_text_from(
173-
self.display_model,
174-
"name",
175-
backward=lambda name: f"Name: {name}",
176-
)
177-
178-
# 2. use refreshable and bind to the attribute's VALUE change
179-
@ui.refreshable
180-
def _person_age_ui() -> None:
181-
ui.label(f"Age: {self.display_model.age}")
182-
183-
_person_age_ui()
184-
self.display_model.on_value_change("age", _person_age_ui.refresh)
185-
186-
# 3. use refreshable and bind to the attribute's TYPE change
187-
@ui.refreshable
188-
def _friend_or_pet_ui() -> None:
189-
if isinstance(self.display_model.companion, Friend):
190-
FriendComponent(self.display_model.companion).display()
191-
192-
elif isinstance(self.display_model.companion, Pet):
193-
PetComponent(self.display_model.companion).display()
194-
195-
_friend_or_pet_ui()
196-
self.display_model.on_type_change("companion", _friend_or_pet_ui.refresh)
162+
with ui.element().classes("border"):
163+
# NOTE:
164+
# There are 3 ways to bind the UI to the model changes:
165+
# 1. using nicegui builting facilties
166+
# 2. via model attribute VALE change
167+
# 3. via model attribute TYPE change
168+
# The model attribute changes allow to trigger re-rendering of subcomponents.
169+
# This should be mainly used for chainging the UI layout based on
170+
# the attribute's value or type.
171+
172+
# 1. bind the label directly to the model's attribute
173+
ui.label().bind_text_from(
174+
self.display_model,
175+
"name",
176+
backward=lambda name: f"Name: {name}",
177+
)
178+
179+
# 2. use refreshable and bind to the attribute's VALUE change
180+
@ui.refreshable
181+
def _person_age_ui() -> None:
182+
ui.label(f"Age: {self.display_model.age}")
183+
184+
_person_age_ui()
185+
self.display_model.on_value_change("age", _person_age_ui.refresh)
186+
187+
# 3. use refreshable and bind to the attribute's TYPE change
188+
@ui.refreshable
189+
def _friend_or_pet_ui() -> None:
190+
if isinstance(self.display_model.companion, Friend):
191+
FriendComponent(self.display_model.companion).display()
192+
193+
elif isinstance(self.display_model.companion, Pet):
194+
PetComponent(self.display_model.companion).display()
195+
196+
_friend_or_pet_ui()
197+
self.display_model.on_type_change("companion", _friend_or_pet_ui.refresh)
197198

198199

199200
async def _ensure_before_corpus(async_page: Page) -> None:
@@ -331,13 +332,17 @@ async def test_multiple_componenets_management(
331332
ensure_page_loaded: Callable[[Callable[[], None]], Awaitable[None]],
332333
async_page: Page,
333334
):
334-
renderer = SomeRenderer[Person](PersonComponent)
335+
renderer = UpdatableComponentList[Person](PersonComponent)
335336

336337
def _index_corpus() -> None:
337338
renderer.display()
338339

339340
await ensure_page_loaded(_index_corpus)
340341

342+
from helpers import take_screenshot
343+
344+
await take_screenshot(async_page, prefix="1.before")
345+
341346
person_1 = Person(name="Alice", age=30, companion=Pet(name="Fluffy", species="cat"))
342347
person_2 = Person(name="Bob", age=25, companion=Friend(name="Marta", age=28))
343348

@@ -350,10 +355,33 @@ def _index_corpus() -> None:
350355
await _ensure_person_is_present(async_page, person_1)
351356
await _ensure_person_is_present(async_page, person_2)
352357

358+
await take_screenshot(async_page, prefix="2.persons-added")
359+
353360
renderer.remove_model("person_1")
354361
await _ensure_person_not_present(async_page, person_1)
355362
await _ensure_person_is_present(async_page, person_2)
356363

364+
await take_screenshot(async_page, prefix="3.person-1-removed")
365+
357366
renderer.remove_model("person_2")
358367
await _ensure_person_not_present(async_page, person_2)
359368
await _ensure_person_not_present(async_page, person_1)
369+
370+
await take_screenshot(async_page, prefix="4.person-2-removed")
371+
372+
renderer.update_from_dict({"person_1": person_1, "person_2": person_2})
373+
await _ensure_person_is_present(async_page, person_1)
374+
await _ensure_person_is_present(async_page, person_2)
375+
376+
await take_screenshot(async_page, prefix="5.persons-added")
377+
378+
renderer.update_from_dict({"person_1": person_1})
379+
await _ensure_person_is_present(async_page, person_1)
380+
await _ensure_person_not_present(async_page, person_2)
381+
382+
await take_screenshot(async_page, prefix="6.person-2-removed")
383+
384+
renderer.update_from_dict({})
385+
await _ensure_person_not_present(async_page, person_1)
386+
await _ensure_person_not_present(async_page, person_2)
387+
await take_screenshot(async_page, prefix="7.person-1-removed")

0 commit comments

Comments
 (0)