aiidalab-chemshell 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """The main AiiDAlab ChemShell python package."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("aiidalab-chemshell")
@@ -0,0 +1 @@
1
+ """Package for general common components."""
@@ -0,0 +1,53 @@
1
+ """Module containing common settings for configuring ChemShell."""
2
+
3
+ from enum import Enum, auto
4
+
5
+
6
+ class BasisSetOptions(Enum):
7
+ """Pre-defined basis set levels for simplified ChemShell inputs."""
8
+
9
+ FAST = 0
10
+ BALANCED = auto()
11
+ QUALITY = auto()
12
+
13
+ @property
14
+ def label(self) -> str:
15
+ """Convert enum value to a string representation for ChemShell input."""
16
+ match self:
17
+ case BasisSetOptions.FAST:
18
+ return "3-21G"
19
+ case BasisSetOptions.BALANCED:
20
+ return "cc-pvdz"
21
+ case BasisSetOptions.QUALITY:
22
+ return "aug-cc-pvtz"
23
+ case "":
24
+ return ""
25
+
26
+
27
+ class WorkflowOptions(Enum):
28
+ """Enum defining the available ChemShell based AiiDA workflows."""
29
+
30
+ GEOMETRY = 0
31
+ NEB = auto()
32
+
33
+ @property
34
+ def label(self) -> str:
35
+ """Convert enum value into a more human readable string."""
36
+ match self:
37
+ case WorkflowOptions.GEOMETRY:
38
+ return "Geometry Optimisation"
39
+ case WorkflowOptions.NEB:
40
+ return "Nudged Elastic Band"
41
+ case _:
42
+ return ""
43
+
44
+ @property
45
+ def tab_label(self) -> str:
46
+ """Create a tab title for the given enum option."""
47
+ match self:
48
+ case WorkflowOptions.GEOMETRY:
49
+ return "Optimisation"
50
+ case WorkflowOptions.NEB:
51
+ return "NEB"
52
+ case _:
53
+ return "ChemShell"
@@ -0,0 +1,170 @@
1
+ """Module for components relating to AiiDA database management."""
2
+
3
+ import datetime
4
+
5
+ import ipywidgets as ipw
6
+ import traitlets as tl
7
+ from aiida.orm import (
8
+ CalcFunctionNode,
9
+ CalcJobNode,
10
+ Node,
11
+ QueryBuilder,
12
+ WorkChainNode,
13
+ )
14
+
15
+
16
+ class AiiDADatabaseWidget(ipw.VBox, tl.HasTraits):
17
+ """Widget for AiiDA database querying."""
18
+
19
+ data_object = tl.Instance(Node, allow_none=True)
20
+
21
+ def __init__(self, title: str = "", query: list | None = None):
22
+ if query is None:
23
+ query = []
24
+ self.title = title
25
+ self.query_type = tuple(query)
26
+
27
+ qbuilder = QueryBuilder().append((CalcJobNode, WorkChainNode), project="label")
28
+
29
+ self.drop_down = ipw.Dropdown(
30
+ options=sorted({"All"}.union({i[0] for i in qbuilder.iterall() if i[0]})),
31
+ value="All",
32
+ description="Process Label",
33
+ disabled=True,
34
+ style={"description_width": "120px"},
35
+ layout={"width": "50%"},
36
+ )
37
+ self.drop_down.observe(self.search, names="value")
38
+
39
+ # Disable process labels selection if we are not looking for the calculated
40
+ # structures.
41
+ def disable_drop_down(change):
42
+ self.drop_down.disabled = not change["new"] == "calculated"
43
+
44
+ # Select structures kind.
45
+ self.mode = ipw.RadioButtons(
46
+ options=["all", "uploaded", "calculated"], layout={"width": "25%"}
47
+ )
48
+ self.mode.observe(self.search, names="value")
49
+ self.mode.observe(disable_drop_down, names="value")
50
+
51
+ # Date range.
52
+ # Note: there is Date picker widget, but it currently does not work in Safari:
53
+ # https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#Date-picker
54
+ date_text = ipw.HTML(value="<p>Select the date range:</p>")
55
+ self.start_date_widget = ipw.Text(
56
+ value="", description="From: ", style={"description_width": "120px"}
57
+ )
58
+ self.end_date_widget = ipw.Text(value="", description="To: ")
59
+
60
+ # Search button.
61
+ btn_search = ipw.Button(
62
+ description="Search",
63
+ button_style="info",
64
+ layout={"width": "initial", "margin": "2px 0 0 2em"},
65
+ )
66
+ btn_search.on_click(self.search)
67
+
68
+ age_selection = ipw.VBox(
69
+ [
70
+ date_text,
71
+ ipw.HBox([self.start_date_widget, self.end_date_widget, btn_search]),
72
+ ],
73
+ layout={"border": "1px solid #fafafa", "padding": "1em"},
74
+ )
75
+
76
+ h_line = ipw.HTML("<hr>")
77
+ box = ipw.VBox([age_selection, h_line, ipw.HBox([self.mode, self.drop_down])])
78
+
79
+ self.results = ipw.Dropdown(layout={"width": "900px"})
80
+ self.results.observe(self._on_select_structure, names="value")
81
+ self.search()
82
+ super().__init__([box, h_line, self.results])
83
+
84
+ def search(self, _=None) -> None:
85
+ """Search structures in the AiiDA database."""
86
+ qbuild = QueryBuilder()
87
+
88
+ # If the date range is valid, use it for the search
89
+ try:
90
+ start_date = datetime.datetime.strptime(
91
+ self.start_date_widget.value, "%Y-%m-%d"
92
+ )
93
+ end_date = datetime.datetime.strptime(
94
+ self.end_date_widget.value, "%Y-%m-%d"
95
+ ) + datetime.timedelta(hours=24)
96
+
97
+ # Otherwise revert to the standard (i.e. last 7 days)
98
+ except ValueError:
99
+ start_date = datetime.datetime.now() - datetime.timedelta(days=7)
100
+ end_date = datetime.datetime.now() + datetime.timedelta(hours=24)
101
+
102
+ self.start_date_widget.value = start_date.strftime("%Y-%m-%d")
103
+ self.end_date_widget.value = end_date.strftime("%Y-%m-%d")
104
+
105
+ filters = {}
106
+ filters["ctime"] = {"and": [{">": start_date}, {"<=": end_date}]}
107
+
108
+ if self.mode.value == "uploaded":
109
+ qbuild2 = (
110
+ QueryBuilder()
111
+ .append(self.query_type, project=["id"], tag="structures")
112
+ .append(Node, with_outgoing="structures")
113
+ )
114
+ processed_nodes = [n[0] for n in qbuild2.all()]
115
+ if processed_nodes:
116
+ filters["id"] = {"!in": processed_nodes}
117
+ qbuild.append(self.query_type, filters=filters)
118
+
119
+ elif self.mode.value == "calculated":
120
+ if self.drop_down.value == "All":
121
+ qbuild.append((CalcJobNode, WorkChainNode), tag="calcjobworkchain")
122
+ else:
123
+ qbuild.append(
124
+ (CalcJobNode, WorkChainNode),
125
+ filters={"label": self.drop_down.value},
126
+ tag="calcjobworkchain",
127
+ )
128
+ qbuild.append(
129
+ self.query_type,
130
+ with_incoming="calcjobworkchain",
131
+ filters=filters,
132
+ )
133
+
134
+ elif self.mode.value == "edited":
135
+ qbuild.append(CalcFunctionNode)
136
+ qbuild.append(
137
+ self.query_type,
138
+ with_incoming=CalcFunctionNode,
139
+ filters=filters,
140
+ )
141
+
142
+ elif self.mode.value == "all":
143
+ qbuild.append(self.query_type, filters=filters)
144
+
145
+ qbuild.order_by({self.query_type: {"ctime": "desc"}})
146
+ matches = {n[0] for n in qbuild.iterall()}
147
+ matches = sorted(matches, reverse=True, key=lambda n: n.ctime)
148
+
149
+ options = [(f"Select a Node ({len(matches)} found)", False)]
150
+ for mch in matches:
151
+ label = f"PK: {mch.pk}"
152
+ label += " | " + mch.ctime.strftime("%Y-%m-%d %H:%M")
153
+ label += " | " + mch.base.extras.get("formula", "")
154
+ label += " | " + mch.node_type.split(".")[-2]
155
+ label += " | " + mch.label
156
+ label += " | " + mch.description
157
+ options.append((label, mch))
158
+
159
+ self.results.options = options
160
+ return
161
+
162
+ def _on_select_structure(self, _) -> None:
163
+ self.data_object = self.results.value or None
164
+ return
165
+
166
+ def disable(self, val: bool) -> None:
167
+ """Disable the widget."""
168
+ self.results.disabled = True
169
+ # self.
170
+ return
@@ -0,0 +1,89 @@
1
+ """Module for providing functionality to deal with files."""
2
+
3
+ from io import BytesIO
4
+
5
+ import traitlets as tl
6
+ from aiida.orm import SinglefileData
7
+ from ipywidgets import FileUpload, HBox, Text
8
+
9
+
10
+ class FileUploadWidget(HBox, tl.HasTraits):
11
+ """A widget for uploading files."""
12
+
13
+ file = tl.Instance(SinglefileData, allow_none=True)
14
+
15
+ def __init__(self, description: str = "File: ", **kwargs):
16
+ """
17
+ FileUploadWidget constructor.
18
+
19
+ Parameters
20
+ ----------
21
+ **kwargs :
22
+ Keyword arguments passed to the parent class's constructor.
23
+ """
24
+ super().__init__(**kwargs)
25
+ self.file_dict = None
26
+
27
+ self.file_upload = FileUpload(
28
+ accept="",
29
+ multiple=False,
30
+ description="Upload",
31
+ layout={"width": "20%"},
32
+ )
33
+ self.file_handle = Text(
34
+ value="",
35
+ placeholder="",
36
+ description=description,
37
+ disabled=True,
38
+ layout={"width": "70%"},
39
+ )
40
+ self.children = [self.file_handle, self.file_upload]
41
+
42
+ self.file_upload.observe(self._on_file_upload, names="value")
43
+
44
+ return
45
+
46
+ @property
47
+ def has_file(self) -> bool:
48
+ """True if a file has been uploaded."""
49
+ return self.file is not None
50
+
51
+ def _on_file_upload(self, _):
52
+ """Handle file upload events."""
53
+ if self.file_upload.value:
54
+ self.file_dict = self.file_upload.value[
55
+ list(self.file_upload.value.keys())[0]
56
+ ]
57
+ self.file_handle.value = self.file_dict["metadata"]["name"]
58
+ self.file = self.get_aiida_file_object()
59
+ else:
60
+ self.file_handle.value = ""
61
+ return
62
+
63
+ def get_file_contents(self) -> BytesIO | None:
64
+ """Get the contents of the uploaded file as a BytesIO object."""
65
+ if self.file_dict is not None:
66
+ return BytesIO(self.file_dict["content"])
67
+ return None
68
+
69
+ def filename(self) -> str:
70
+ """Get the name of the uploaded file."""
71
+ if self.file_dict is not None:
72
+ return self.file_dict["metadata"]["name"]
73
+ return ""
74
+
75
+ def get_aiida_file_object(self):
76
+ """Get the uploaded file as an AiiDA SinglefileData object."""
77
+ if self.file_dict is not None:
78
+ return SinglefileData(
79
+ file=self.get_file_contents(),
80
+ filename=self.filename(),
81
+ label=self.filename(),
82
+ description=self.file_handle.description,
83
+ )
84
+ return None
85
+
86
+ def disable(self, val: bool) -> None:
87
+ """Disable the file upload widget."""
88
+ self.file_upload.disabled = val
89
+ return
@@ -0,0 +1,81 @@
1
+ """Module handling navigation controls within the app."""
2
+
3
+ from functools import partial
4
+
5
+ import ipywidgets as ipw
6
+
7
+ from aiidalab_chemshell.utils import open_link_in_new_tab
8
+
9
+ _APPS_DIRECTORY = "/apps/apps/"
10
+
11
+
12
+ class QuickAccessButtons(ipw.HBox):
13
+ """Quick access buttons present in the apps header and start banner."""
14
+
15
+ def __init__(self, **kwargs):
16
+ """
17
+ QuickAccessButtons constructor.
18
+
19
+ Parameters
20
+ ----------
21
+ **kwargs :
22
+ Keyword arguments passed to the `ipywidgets.HBox.__init__()`.
23
+ """
24
+ self.new_calc_link = ipw.Button(
25
+ description="New Calculation",
26
+ disabled=False,
27
+ button_style="success",
28
+ tooltip="Start a new calculation",
29
+ icon="plus",
30
+ )
31
+ self.new_calc_link.on_click(
32
+ partial(
33
+ open_link_in_new_tab,
34
+ _APPS_DIRECTORY + "aiidalab-chemshell/notebooks/main.ipynb",
35
+ )
36
+ )
37
+
38
+ self.history_link = ipw.Button(
39
+ description="History",
40
+ disabled=False,
41
+ button_style="primary",
42
+ tooltip="View Calculation History",
43
+ icon="history",
44
+ )
45
+ self.history_link.on_click(
46
+ partial(
47
+ open_link_in_new_tab,
48
+ _APPS_DIRECTORY + "aiidalab-chemshell/notebooks/history.ipynb",
49
+ )
50
+ )
51
+
52
+ self.resource_setup_link = ipw.Button(
53
+ description="Setup Resources",
54
+ disabled=False,
55
+ button_style="primary",
56
+ tooltip="Configure Computational Resources",
57
+ icon="cogs",
58
+ )
59
+ self.resource_setup_link.on_click(
60
+ partial(open_link_in_new_tab, _APPS_DIRECTORY + "home/code_setup.ipynb")
61
+ )
62
+
63
+ self.docs_link = ipw.Button(
64
+ description="Documentation",
65
+ disabled=False,
66
+ button_style="info",
67
+ tooltip="Open Documentation",
68
+ icon="book",
69
+ )
70
+ self.docs_link.on_click(
71
+ partial(open_link_in_new_tab, "https://github.com/stfc/aiidalab-chemshell")
72
+ )
73
+
74
+ children = [
75
+ self.new_calc_link,
76
+ self.history_link,
77
+ self.resource_setup_link,
78
+ self.docs_link,
79
+ ]
80
+ super().__init__(children=children, layout={"margin": "auto"}, **kwargs)
81
+ return
@@ -0,0 +1,166 @@
1
+ """Defines a custom AiiDA node visualiser."""
2
+
3
+ from pathlib import Path
4
+ from tempfile import NamedTemporaryFile
5
+
6
+ import ase
7
+ from aiida.orm import ArrayData, Node, ProcessNode, SinglefileData, StructureData
8
+ from aiidalab_widgets_base.loaders import LoadingWidget
9
+ from aiidalab_widgets_base.viewers import AIIDA_VIEWER_MAPPING
10
+ from IPython.display import clear_output, display
11
+ from ipywidgets import HTML, DOMWidget, Dropdown, Output, VBox
12
+ from traitlets import Instance, observe
13
+
14
+
15
+ class CustomAiidaNodeViewWidget(VBox):
16
+ """
17
+ Custom viewer based on a specific AiiDA node type.
18
+
19
+ An extension of the aiida_widgets_base.viewers.AiidaNodeViewWidget
20
+ enabling more customisability when registering viewers with nodes
21
+ returned from ChemShell jobs. The main outline is taken from the base
22
+ aiidalab_widgets_base viewer with an extended viewer() method which
23
+ allows handling of node types which the base viewer has no registered
24
+ visualisation widgets.
25
+ """
26
+
27
+ node = Instance(Node, allow_none=True)
28
+
29
+ def __init__(self, **kwargs):
30
+ """CustomAiidaNodeViewWidget Constructor."""
31
+ self._output = Output()
32
+ self.node_views = {}
33
+ self.node_view_loading_message = LoadingWidget("Loading Node View")
34
+ super().__init__(**kwargs)
35
+ self.add_class("aiida-node-view-widget")
36
+
37
+ @observe("node")
38
+ def _observe_node(self, change):
39
+ if not ((node := change["new"]) and node != change["old"]):
40
+ return
41
+ if node.uuid in self.node_views:
42
+ self.children = [self.node_views[node.uuid]]
43
+ return
44
+ self.children = [self.node_view_loading_message]
45
+ node_view = self._viewer(node)
46
+ if isinstance(node_view, DOMWidget):
47
+ self.node_views[node.uuid] = node_view
48
+ self.children = [node_view]
49
+ else:
50
+ with self._output:
51
+ clear_output()
52
+ if change["new"]:
53
+ display(node_view)
54
+ self.children = [self._output]
55
+
56
+ def _viewer(self, node: Node, **kwargs):
57
+ """Create a viewer based on the type of Node being visualised."""
58
+ _viewer = AIIDA_VIEWER_MAPPING.get(node.node_type)
59
+ if isinstance(node, ProcessNode):
60
+ # Allow to register specific viewers based on node.process_type
61
+ _viewer = AIIDA_VIEWER_MAPPING.get(node.process_type, _viewer)
62
+
63
+ if _viewer:
64
+ return _viewer(node, **kwargs)
65
+
66
+ # Handle custom ChemShell specific visualisation
67
+ if isinstance(node, SinglefileData):
68
+ # Singlefile data output generally refers to a structure file
69
+ # output from ChemShell jobs
70
+ structure = self._get_structure_data_object_from_file(
71
+ node.filename, node.content
72
+ )
73
+ if structure:
74
+ _viewer = AIIDA_VIEWER_MAPPING.get(structure.node_type)
75
+ if _viewer:
76
+ return _viewer(structure, **kwargs)
77
+
78
+ if isinstance(node, ArrayData):
79
+ return AiidaArrayDataViewWidget(node, **kwargs)
80
+ # No viewer registered for this type, return node itself
81
+ return node
82
+
83
+ def _get_structure_data_object_from_file(
84
+ self, fname: str, content: bytes
85
+ ) -> StructureData | None:
86
+ """
87
+ Create an ase strucure object from a structure file.
88
+
89
+ Parameters
90
+ ----------
91
+ fname : str
92
+ The file name the structure is being read from.
93
+ content : bytes
94
+ The content of the structure file byte encoded.
95
+
96
+ Return
97
+ ------
98
+ structure : ase.Atoms | None
99
+ The ASE atomic structure object or None if the file could not be read.
100
+ """
101
+ suffix = "".join(Path(fname).suffixes)
102
+ with NamedTemporaryFile(suffix=suffix) as tmpf:
103
+ tmpf.write(content)
104
+ tmpf.flush()
105
+ try:
106
+ structure = ase.io.read(tmpf.name, index=":")[0]
107
+ except (KeyError, ase.io.formats.UnknownFileTypeError):
108
+ node = None
109
+ else:
110
+ # ASE doesn't correctly interpret atomic units so convert all units to
111
+ # angstrom
112
+ for i in range(len(structure)):
113
+ structure.positions[i] = structure.positions[i] * 0.529177
114
+ node = StructureData()
115
+ node.set_ase(structure)
116
+ return node
117
+
118
+
119
+ class AiidaArrayDataViewWidget(VBox):
120
+ """Custom widget to display array data produced from ChemShell jobs."""
121
+
122
+ def __init__(self, array: ArrayData, **kwargs):
123
+ """AiidaArrayDataViewWidget Constructor.
124
+
125
+ Parameters
126
+ ----------
127
+ array : ArrayData
128
+ The AiiDA ArrayData object to display.
129
+ """
130
+ super().__init__(**kwargs)
131
+ self.array = array
132
+ self.array_names = array.get_arraynames()
133
+
134
+ self.array_selector = Dropdown(
135
+ options=self.array_names,
136
+ description="Array Label:",
137
+ disabled=False,
138
+ layout={"width": "30%"},
139
+ )
140
+ self._render_array({"new": self.array_selector.index, "old": -1})
141
+ self.array_selector.observe(self._render_array, "index")
142
+
143
+ return
144
+
145
+ def _render_array(self, change) -> None:
146
+ """Create a HTML table based on the currently selected array."""
147
+ index = change["new"]
148
+ if index == change["old"]:
149
+ return
150
+ values = self.array.get_array(self.array_names[index])
151
+ # Construct HTML Table
152
+ html = "<table style='width:100%; border: 1px solid #ddd; text-align: left; "
153
+ html += "border-collapse: collapse;'>"
154
+ html += "<tr style='background-color: #2196F3; color: white;'>"
155
+ html += "<th>Atom Index</th><th>X</th><th>Y</th><th>Z</th></tr>"
156
+
157
+ for idx, row in enumerate(values):
158
+ bg_color = "#f9f9f9" if idx % 2 == 0 else "#ffffff"
159
+ html += f"<tr style='background-color: {bg_color};'>"
160
+ html += f"<td><b>{idx}</b></td><td>{row[0]:.6f}</td><td>{row[1]:.6f}</td>"
161
+ html += f"<td>{row[2]:.6f}</td>"
162
+ html += "</tr>"
163
+ html += "</table>"
164
+
165
+ self.children = [self.array_selector, HTML(html)]
166
+ return
@@ -0,0 +1,124 @@
1
+ """Defines the process history applicaion page."""
2
+
3
+ from datetime import datetime
4
+
5
+ from aiida.orm import CalcJobNode, WorkChainNode
6
+ from aiidalab_widgets_base import ProcessNodesTreeWidget
7
+ from IPython.display import display
8
+ from ipywidgets import HTML, VBox, dlink
9
+
10
+ from aiidalab_chemshell.common.database import AiiDADatabaseWidget
11
+ from aiidalab_chemshell.common.navigation import QuickAccessButtons
12
+ from aiidalab_chemshell.common.node_viewers import CustomAiidaNodeViewWidget
13
+ from aiidalab_chemshell.models.process import ProcessModel
14
+
15
+
16
+ class HistoryApp:
17
+ """The process history page's main app."""
18
+
19
+ def __init__(self):
20
+ """HistoryApp constructor."""
21
+ self.model = HistoryModel()
22
+ self.view = HistoryAppView(self.model)
23
+ display(self.view)
24
+
25
+
26
+ class HistoryModel(ProcessModel):
27
+ """MVC Model for process history app data management."""
28
+
29
+ pass
30
+
31
+
32
+ class HistoryAppView(VBox):
33
+ """Main view for the process history page."""
34
+
35
+ def __init__(self, model: HistoryModel, **kwargs):
36
+ """
37
+ HistoryAppView Constructor.
38
+
39
+ Parameters
40
+ ----------
41
+ model : HistoryModel
42
+ The MVC model component to associate with this view app.
43
+ """
44
+ self.model = model
45
+ logo = HTML(
46
+ """
47
+ <div class="app-container logo" style="width: 300px;">
48
+ <img src="../images/alc.svg" alt="ALC AiiDAlab App Logo" />
49
+ </div>
50
+ """,
51
+ layout={"margin": "auto"},
52
+ )
53
+
54
+ subtitle = HTML(
55
+ """
56
+ <h2 id='subtitle'>AiiDAlab ChemShell</h2>
57
+ """
58
+ )
59
+
60
+ nav_btns = QuickAccessButtons()
61
+
62
+ header = VBox(
63
+ children=[
64
+ logo,
65
+ subtitle,
66
+ ],
67
+ layout={"margin": "auto"},
68
+ )
69
+
70
+ footer = HTML(
71
+ f"""
72
+ <footer>
73
+ Copyright (c) {datetime.now().year} Ada Lovelace Centre
74
+ (STFC) <br>
75
+ </footer>
76
+ """,
77
+ layout={"align-content": "right"},
78
+ )
79
+ h_line = HTML("<hr>")
80
+
81
+ self.guide = HTML(
82
+ """
83
+ <h3>ChemShell Process History</h3>
84
+ <p>
85
+ Search through past ChemShell processes and visualise inputs, outputs and
86
+ provenance relationships.
87
+ </p>
88
+ """
89
+ )
90
+ self.lookup_widget = AiiDADatabaseWidget(
91
+ "Process Lookup", [CalcJobNode, WorkChainNode]
92
+ )
93
+ self.lookup_widget.observe(self._update_node_view, "data_object")
94
+
95
+ self.node_tree = ProcessNodesTreeWidget()
96
+ dlink((self.model, "process_uuid"), (self.node_tree, "value"))
97
+ self.node_view = CustomAiidaNodeViewWidget()
98
+ dlink(
99
+ (self.node_tree, "selected_nodes"),
100
+ (self.node_view, "node"),
101
+ transform=lambda nodes: nodes[0] if nodes else None,
102
+ )
103
+
104
+ super().__init__(
105
+ layout={},
106
+ children=[
107
+ header,
108
+ nav_btns,
109
+ self.guide,
110
+ self.lookup_widget,
111
+ h_line,
112
+ self.node_tree,
113
+ self.node_view,
114
+ footer,
115
+ ],
116
+ **kwargs,
117
+ )
118
+ return
119
+
120
+ def _update_node_view(self, _) -> None:
121
+ """Update the node view to the currently selected process node."""
122
+ if self.lookup_widget.data_object is not None:
123
+ self.model.process_uuid = self.lookup_widget.data_object.uuid
124
+ return