Skip to content

Commit 9016929

Browse files
yt-msMidnighter
authored andcommitted
feat(DeploymentView): add animation support
1 parent c997c67 commit 9016929

File tree

4 files changed

+184
-10
lines changed

4 files changed

+184
-10
lines changed

src/structurizr/view/animation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ class Animation(AbstractBase):
4646
Define a wrapper for a collection of animation steps.
4747
4848
Attributes:
49-
order:
50-
elements:
51-
relationships:
49+
order: the order in which this animation step appears
50+
elements: the IDs of the elements to show in this step
51+
relationships: ths IDs of the relationships to show in this step
5252
5353
"""
5454

src/structurizr/view/deployment_view.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
from ..mixin.model_ref_mixin import ModelRefMixin
2222
from ..model.container_instance import ContainerInstance
2323
from ..model.deployment_node import DeploymentNode
24+
from ..model.element import Element
2425
from ..model.infrastructure_node import InfrastructureNode
2526
from ..model.relationship import Relationship
2627
from ..model.software_system_instance import SoftwareSystemInstance
28+
from ..model.static_structure_element import StaticStructureElement
2729
from .animation import Animation
2830
from .view import View
2931

@@ -95,6 +97,11 @@ def add(
9597
else:
9698
pass # TODO
9799

100+
def __iadd__(self, item: Union[DeploymentNode, Relationship]):
101+
"""Add a deployment node or relationship to this view."""
102+
self.add(item)
103+
return self
104+
98105
def remove(
99106
self,
100107
item: Union[
@@ -147,15 +154,77 @@ def name(self):
147154
name = f"{name} - {self.environment}"
148155
return name
149156

150-
# def can_be_removed(element: Element)
157+
def add_animation(
158+
self, *element_instances: Union[StaticStructureElement, InfrastructureNode]
159+
):
160+
"""Add an animation step, with the given elements and infrastructure nodes."""
161+
if len(element_instances) == 0:
162+
raise ValueError(
163+
"One or more software system/container instances and/or "
164+
+ "infrastructure nodes must be specified"
165+
)
166+
167+
element_ids_in_previous_steps = set()
168+
for step in self.animations:
169+
element_ids_in_previous_steps = element_ids_in_previous_steps.union(
170+
step.elements
171+
)
151172

152-
# def add_animation(element_instances)
173+
element_ids_in_this_step = set()
174+
relationship_ids_in_this_step = set()
175+
176+
for element in element_instances:
177+
if (
178+
self.is_element_in_view(element)
179+
and element.id not in element_ids_in_previous_steps
180+
):
181+
element_ids_in_previous_steps.add(element.id)
182+
element_ids_in_this_step.add(element.id)
183+
184+
deployment_node = self._find_deployment_node(element)
185+
while deployment_node is not None:
186+
if deployment_node.id not in element_ids_in_previous_steps:
187+
element_ids_in_previous_steps.add(deployment_node.id)
188+
element_ids_in_this_step.add(deployment_node.id)
189+
deployment_node = deployment_node.parent
190+
191+
if element_ids_in_this_step == set():
192+
raise ValueError(
193+
"None of the specified container instances exist in this view."
194+
)
153195

154-
# def add_animation_step(elements)
196+
for relationship_view in self.relationship_views:
197+
relationship = relationship_view.relationship
198+
if (
199+
relationship.source.id in element_ids_in_this_step
200+
and relationship.destination.id in element_ids_in_previous_steps
201+
) or (
202+
relationship.destination.id in element_ids_in_this_step
203+
and relationship.source.id in element_ids_in_previous_steps
204+
):
205+
relationship_ids_in_this_step.add(relationship.id)
206+
207+
self._animations.append(
208+
Animation(
209+
order=len(self._animations) + 1,
210+
elements=element_ids_in_this_step,
211+
relationships=relationship_ids_in_this_step,
212+
)
213+
)
155214

156-
# def _find_deployment_node(element: Element)
215+
def _find_deployment_node(self, element: Element) -> DeploymentNode:
216+
all_deployment_nodes = [
217+
e for e in self.model.get_elements() if isinstance(e, DeploymentNode)
218+
]
219+
for node in all_deployment_nodes:
220+
if (
221+
element in node.container_instances
222+
or element in node.infrastructure_nodes
223+
):
224+
return node
225+
return None
157226

158227
@property
159228
def animations(self) -> Iterable[Animation]:
160229
"""Return the animations for this view."""
161-
pass # TODO
230+
return list(self._animations)

src/structurizr/view/view.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ def copy_layout_information_from(self, source: "View") -> None:
208208
source_relationship_view
209209
)
210210

211+
def is_element_in_view(self, element: Element) -> bool:
212+
"""Return True if the given element is in this view."""
213+
return any([e.element.id == element.id for e in self.element_views])
214+
211215
def find_element_view(
212216
self, source_element_view: ElementView
213217
) -> Optional[ElementView]:

tests/unit/view/test_deployment_view.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,115 @@ def test_deployment_view_add_deployment_node_adds_parent(empty_workspace: Worksp
188188
deployment_view = empty_workspace.views.create_deployment_view(
189189
software_system=software_system, key="deployment", description="Description"
190190
)
191-
deployment_view.add(child_deployment_node)
191+
deployment_view += child_deployment_node
192192
element_views = deployment_view.element_views
193193
assert len(element_views) == 3
194194
assert any([x.element is parent_deployment_node for x in element_views])
195195
assert any([x.element is child_deployment_node for x in element_views])
196196
assert any([x.element is container_instance for x in element_views])
197197

198198

199-
# TODO: Animations
199+
def test_add_animation_step_raises_if_no_elements(empty_workspace: Workspace):
200+
"""Check error handling if no elements passed."""
201+
deployment_view = empty_workspace.views.create_deployment_view(
202+
key="deployment", description="Description"
203+
)
204+
with pytest.raises(ValueError):
205+
deployment_view.add_animation()
206+
207+
208+
def test_add_animation_step(empty_workspace: Workspace):
209+
"""Check happy path."""
210+
model = empty_workspace.model
211+
views = empty_workspace.views
212+
213+
software_system = model.add_software_system("Software System")
214+
web_application = software_system.add_container("Web Application")
215+
database = software_system.add_container("Database")
216+
web_application.uses(database, "Reads from and writes to", "JDBC/HTTPS")
217+
218+
developer_laptop = model.add_deployment_node("Developer Laptop")
219+
apache_tomcat = developer_laptop.add_deployment_node("Apache Tomcat")
220+
oracle = developer_laptop.add_deployment_node("Oracle")
221+
web_application_instance = apache_tomcat.add_container(web_application)
222+
database_instance = oracle.add_container(database)
223+
224+
deployment_view = views.create_deployment_view(
225+
software_system=software_system, key="deployment", description="Description"
226+
)
227+
deployment_view += developer_laptop
228+
229+
deployment_view.add_animation(web_application_instance)
230+
deployment_view.add_animation(database_instance)
231+
232+
step1 = deployment_view.animations[0]
233+
assert step1.order == 1
234+
assert len(step1.elements) == 3
235+
assert developer_laptop.id in step1.elements
236+
assert apache_tomcat.id in step1.elements
237+
assert web_application_instance.id in step1.elements
238+
assert len(step1.relationships) == 0
239+
240+
step2 = deployment_view.animations[1]
241+
assert step2.order == 2
242+
assert len(step2.elements) == 2
243+
assert oracle.id in step2.elements
244+
assert database_instance.id in step2.elements
245+
assert len(step2.relationships) == 1
246+
assert next(iter(web_application_instance.relationships)).id in step2.relationships
247+
248+
249+
def test_animation_ignores_containers_outside_this_view(empty_workspace: Workspace):
250+
"""Check that containers outside this view are ignored when adding animations."""
251+
model = empty_workspace.model
252+
views = empty_workspace.views
253+
254+
software_system = model.add_software_system("Software System")
255+
web_application = software_system.add_container("Web Application")
256+
database = software_system.add_container("Database")
257+
web_application.uses(database, "Reads from and writes to", "JDBC/HTTPS")
258+
259+
developer_laptop = model.add_deployment_node("Developer Laptop")
260+
apache_tomcat = developer_laptop.add_deployment_node("Apache Tomcat")
261+
oracle = developer_laptop.add_deployment_node("Oracle")
262+
web_application_instance = apache_tomcat.add_container(web_application)
263+
database_instance = oracle.add_container(database)
264+
265+
deployment_view = views.create_deployment_view(
266+
software_system=software_system, key="deployment", description="Description"
267+
)
268+
deployment_view += apache_tomcat
269+
270+
# database_instance isn't in the view this time
271+
deployment_view.add_animation(web_application_instance, database_instance)
272+
273+
step1 = deployment_view.animations[0]
274+
assert developer_laptop.id in step1.elements
275+
assert database.id not in step1.elements
276+
277+
278+
def test_animation_raises_if_no_container_instances_found(empty_workspace: Workspace):
279+
"""Check error raised if no container instance exists in the view."""
280+
model = empty_workspace.model
281+
views = empty_workspace.views
282+
283+
software_system = model.add_software_system("Software System")
284+
web_application = software_system.add_container("Web Application")
285+
database = software_system.add_container("Database")
286+
web_application.uses(database, "Reads from and writes to", "JDBC/HTTPS")
287+
288+
developer_laptop = model.add_deployment_node("Developer Laptop")
289+
apache_tomcat = developer_laptop.add_deployment_node("Apache Tomcat")
290+
oracle = developer_laptop.add_deployment_node("Oracle")
291+
web_application_instance = apache_tomcat.add_container(web_application)
292+
database_instance = oracle.add_container(database)
293+
294+
deployment_view = views.create_deployment_view(
295+
software_system=software_system, key="deployment", description="Description"
296+
)
297+
298+
with pytest.raises(ValueError):
299+
deployment_view.add_animation(web_application_instance, database_instance)
300+
200301

201302
# TODO: Removing

0 commit comments

Comments
 (0)