ivoryos 1.0.9__py3-none-any.whl → 1.4.4__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.
Files changed (107) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +17 -207
  3. ivoryos/app.py +154 -0
  4. ivoryos/config.py +1 -0
  5. ivoryos/optimizer/ax_optimizer.py +191 -0
  6. ivoryos/optimizer/base_optimizer.py +84 -0
  7. ivoryos/optimizer/baybe_optimizer.py +193 -0
  8. ivoryos/optimizer/nimo_optimizer.py +173 -0
  9. ivoryos/optimizer/registry.py +11 -0
  10. ivoryos/routes/auth/auth.py +43 -14
  11. ivoryos/routes/auth/templates/change_password.html +32 -0
  12. ivoryos/routes/control/control.py +101 -366
  13. ivoryos/routes/control/control_file.py +33 -0
  14. ivoryos/routes/control/control_new_device.py +152 -0
  15. ivoryos/routes/control/templates/controllers.html +193 -0
  16. ivoryos/routes/control/templates/controllers_new.html +112 -0
  17. ivoryos/routes/control/utils.py +40 -0
  18. ivoryos/routes/data/data.py +197 -0
  19. ivoryos/routes/data/templates/components/step_card.html +78 -0
  20. ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
  21. ivoryos/routes/data/templates/workflow_view.html +360 -0
  22. ivoryos/routes/design/__init__.py +4 -0
  23. ivoryos/routes/design/design.py +348 -657
  24. ivoryos/routes/design/design_file.py +68 -0
  25. ivoryos/routes/design/design_step.py +171 -0
  26. ivoryos/routes/design/templates/components/action_form.html +53 -0
  27. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  28. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  29. ivoryos/routes/design/templates/components/canvas.html +5 -0
  30. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  31. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  32. ivoryos/routes/design/templates/components/canvas_main.html +39 -0
  33. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  34. ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
  35. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  36. ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
  37. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  38. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  39. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  40. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  41. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  42. ivoryos/routes/design/templates/components/modals.html +6 -0
  43. ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  44. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  45. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  46. ivoryos/routes/design/templates/experiment_builder.html +44 -0
  47. ivoryos/routes/execute/__init__.py +0 -0
  48. ivoryos/routes/execute/execute.py +377 -0
  49. ivoryos/routes/execute/execute_file.py +78 -0
  50. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  51. ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  52. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  53. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  54. ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  55. ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  56. ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  57. ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
  58. ivoryos/routes/execute/templates/experiment_run.html +30 -0
  59. ivoryos/routes/library/__init__.py +0 -0
  60. ivoryos/routes/library/library.py +157 -0
  61. ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
  62. ivoryos/routes/main/main.py +31 -3
  63. ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
  64. ivoryos/server.py +180 -0
  65. ivoryos/socket_handlers.py +52 -0
  66. ivoryos/static/ivoryos_logo.png +0 -0
  67. ivoryos/static/js/action_handlers.js +384 -0
  68. ivoryos/static/js/db_delete.js +23 -0
  69. ivoryos/static/js/script_metadata.js +39 -0
  70. ivoryos/static/js/socket_handler.js +40 -5
  71. ivoryos/static/js/sortable_design.js +107 -56
  72. ivoryos/static/js/ui_state.js +114 -0
  73. ivoryos/templates/base.html +67 -8
  74. ivoryos/utils/bo_campaign.py +180 -3
  75. ivoryos/utils/client_proxy.py +267 -36
  76. ivoryos/utils/db_models.py +300 -65
  77. ivoryos/utils/decorators.py +34 -0
  78. ivoryos/utils/form.py +63 -29
  79. ivoryos/utils/global_config.py +34 -1
  80. ivoryos/utils/nest_script.py +314 -0
  81. ivoryos/utils/py_to_json.py +295 -0
  82. ivoryos/utils/script_runner.py +599 -165
  83. ivoryos/utils/serilize.py +201 -0
  84. ivoryos/utils/task_runner.py +71 -21
  85. ivoryos/utils/utils.py +50 -6
  86. ivoryos/version.py +1 -1
  87. ivoryos-1.4.4.dist-info/METADATA +263 -0
  88. ivoryos-1.4.4.dist-info/RECORD +119 -0
  89. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
  90. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
  91. tests/unit/test_type_conversion.py +42 -0
  92. tests/unit/test_util.py +3 -0
  93. ivoryos/routes/control/templates/control/controllers.html +0 -78
  94. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  95. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  96. ivoryos/routes/database/database.py +0 -306
  97. ivoryos/routes/database/templates/database/step_card.html +0 -7
  98. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  99. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  100. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  101. ivoryos-1.0.9.dist-info/METADATA +0 -218
  102. ivoryos-1.0.9.dist-info/RECORD +0 -61
  103. /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
  104. /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
  105. /ivoryos/routes/{database → data}/__init__.py +0 -0
  106. /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
  107. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
@@ -1,89 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | New devices{% endblock %}
3
-
4
- {% block body %}
5
- <div class="row">
6
- <div class="col-xl-4 col-lg-4 col-md-6 mb-4 ">
7
- <h5>Available Python API</h5>
8
- <hr>
9
- {% for instrument in api_variables %}
10
- <div class="bg-white rounded shadow-sm position-relative">
11
- <h5 class="p-3 controller-card">
12
- <a href="{{ url_for('control.new_controller', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a>
13
- </h5>
14
- </div>
15
- {% endfor %}
16
- <div class="bg-white rounded shadow-sm position-relative">
17
- <h5 class="p-3 controller-card">
18
- <a data-bs-toggle="modal" href="#importAPI" class="stretched-link"><i class="bi bi-folder-plus"></i> Import API</a>
19
- </h5>
20
- </div>
21
- </div>
22
- <div class="col-xl-5 col-lg-5 col-md-6 mb-4 ">
23
- {% if device %}
24
- <h5>Connecting</h5><hr>
25
- <form role="form" method='POST' name="init" action="{{ url_for('control.new_controller', instrument=instrument) }}">
26
- <div class="form-group">
27
- <div class="input-group mb-3">
28
- <span class="input-group-text" >Name this device</span>
29
- <input class="form-control" type="text" id="device_name" name="device_name" aria-labelledby="nameHelpBlock" placeholder="e.g. {{device.__name__}}_1" >
30
- <div id="nameHelpBlock" class="form-text">
31
- Name your instrument, avoid names that are defined on the right
32
- </div>
33
- </div>
34
- {% for arg in device.__init__.__annotations__ %}
35
- {% if not arg == "return" %}
36
- <div class="input-group mb-3">
37
- <span class="input-group-text" >{{arg}}</span>
38
- <input class="form-control" type="text" id="{{arg}}" name="{{arg}}"
39
- placeholder="{{device.__init__.__annotations__[arg].__name__}}"
40
- value="{{args.parameters[arg].default if not args.parameters[arg].default.__name__ == '_empty' else ''}}">
41
- {% if device.__init__.__annotations__[arg].__module__ is not in ["builtins", "typing"] %}
42
- <a role="button" href="{{ url_for('control.new_controller', instrument=device.__init__.__annotations__[arg].__name__) }}" class="btn btn-secondary">initialize {{device.__init__.__annotations__[arg].__name__}} first</a>
43
- {% endif %}
44
- </div>
45
- {% endif %}
46
- {% endfor %}
47
- <button type="submit" class="btn btn-dark">Connect</button>
48
- </div>
49
- </form>
50
- {% endif %}
51
- </div>
52
- <div class="col-xl-3 col-lg-3 col-md-6 mb-4">
53
- <h5>Defined Instruments</h5><hr>
54
- {% if defined_variables %}
55
- <ul class="list-group">
56
- {% for instrument in defined_variables %}
57
- <li class="list-group-item">{{instrument}}</li>
58
- {% endfor %}
59
- </ul>
60
- {% endif %}
61
- </div>
62
- </div>
63
-
64
-
65
- <div class="modal fade" id="importAPI" tabindex="-1" aria-labelledby="importModal" aria-hidden="true" >
66
- <div class="modal-dialog">
67
- <div class="modal-content">
68
- <div class="modal-header">
69
- <h1 class="modal-title fs-5" id="importModal">Import API by file path</h1>
70
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
71
- </div>
72
- <form method="POST" action="{{ url_for('control.import_api') }}" enctype="multipart/form-data">
73
- <div class="modal-body">
74
- <h5>input manually</h5>
75
- <div class="input-group mb-3">
76
- <label class="input-group-text" for="filepath">File Path:</label>
77
- <input type="text" class="form-control" name="filepath" id="filepath">
78
- </div>
79
- <div class="modal-footer">
80
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
81
- <button type="submit" class="btn btn-primary"> Save </button>
82
- </div>
83
- </div>
84
- </form>
85
- </div>
86
- </div>
87
- </div>
88
-
89
- {% endblock %}
@@ -1,306 +0,0 @@
1
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
2
- from flask_login import login_required
3
-
4
- from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
5
- from ivoryos.utils.utils import get_script_file, post_script_file
6
-
7
- database = Blueprint('database', __name__, template_folder='templates/database')
8
-
9
-
10
-
11
- @database.route("/database/scripts/edit/<script_name>")
12
- @login_required
13
- def edit_workflow(script_name:str):
14
- """
15
- .. :quickref: Database; load workflow script to canvas
16
-
17
- load the selected workflow to the design canvas
18
-
19
- .. http:get:: /database/scripts/edit/<script_name>
20
-
21
- :param script_name: script name
22
- :type script_name: str
23
- :status 302: redirect to :http:get:`/ivoryos/design/script/`
24
- """
25
- row = Script.query.get(script_name)
26
- script = Script(**row.as_dict())
27
- post_script_file(script)
28
- pseudo_name = session.get("pseudo_deck", "")
29
- off_line = current_app.config["OFF_LINE"]
30
- if off_line and pseudo_name and not script.deck == pseudo_name:
31
- flash(f"Choose the deck with name {script.deck}")
32
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
33
- return jsonify({
34
- "script": script.as_dict(),
35
- "python_script": script.compile(),
36
- })
37
- return redirect(url_for('design.experiment_builder'))
38
-
39
-
40
- @database.route("/database/scripts/delete/<script_name>")
41
- @login_required
42
- def delete_workflow(script_name: str):
43
- """
44
- .. :quickref: Database; delete workflow
45
-
46
- delete workflow from database
47
-
48
- .. http:get:: /database/scripts/delete/<script_name>
49
-
50
- :param script_name: workflow name
51
- :type script_name: str
52
- :status 302: redirect to :http:get:`/ivoryos/database/scripts/`
53
-
54
- """
55
- Script.query.filter(Script.name == script_name).delete()
56
- db.session.commit()
57
- return redirect(url_for('database.load_from_database'))
58
-
59
-
60
- @database.route("/database/scripts/save")
61
- @login_required
62
- def publish():
63
- """
64
- .. :quickref: Database; save workflow to database
65
-
66
- save workflow to database
67
-
68
- .. http:get:: /database/scripts/save
69
-
70
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
71
- """
72
- script = get_script_file()
73
- if not script.name or not script.deck:
74
- flash("Deck cannot be empty, try to re-submit deck configuration on the left panel")
75
- row = Script.query.get(script.name)
76
- if row and row.status == "finalized":
77
- flash("This is a protected script, use save as to rename.")
78
- elif row and not session['user'] == row.author:
79
- flash("You are not the author, use save as to rename.")
80
- else:
81
- db.session.merge(script)
82
- db.session.commit()
83
- flash("Saved!")
84
- return redirect(url_for('design.experiment_builder'))
85
-
86
-
87
- @database.route("/database/scripts/finalize")
88
- @login_required
89
- def finalize():
90
- """
91
- .. :quickref: Database; finalize the workflow
92
-
93
- [protected workflow] prevent saving edited workflow to the same workflow name
94
-
95
- .. http:get:: /finalize
96
-
97
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
98
-
99
- """
100
- script = get_script_file()
101
- script.finalize()
102
- if script.name:
103
- db.session.merge(script)
104
- db.session.commit()
105
- post_script_file(script)
106
- return redirect(url_for('design.experiment_builder'))
107
-
108
-
109
- @database.route("/database/scripts/", strict_slashes=False)
110
- @database.route("/database/scripts/<deck_name>")
111
- @login_required
112
- def load_from_database(deck_name=None):
113
- """
114
- .. :quickref: Database; database page
115
-
116
- backend control through http requests
117
-
118
- .. http:get:: /database/scripts/<deck_name>
119
-
120
- :param deck_name: filter for deck name
121
- :type deck_name: str
122
-
123
- """
124
- session.pop('edit_action', None) # reset cache
125
- query = Script.query
126
- search_term = request.args.get("keyword", None)
127
- if search_term:
128
- query = query.filter(Script.name.like(f'%{search_term}%'))
129
- if deck_name is None:
130
- temp = Script.query.with_entities(Script.deck).distinct().all()
131
- deck_list = [i[0] for i in temp]
132
- else:
133
- query = query.filter(Script.deck == deck_name)
134
- deck_list = ["ALL"]
135
- page = request.args.get('page', default=1, type=int)
136
- per_page = 10
137
-
138
- scripts = query.paginate(page=page, per_page=per_page, error_out=False)
139
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
140
- scripts = query.all()
141
- script_names = [script.name for script in scripts]
142
- return jsonify({
143
- "workflows": script_names,
144
- })
145
- else:
146
- # return HTML
147
- return render_template("scripts_database.html", scripts=scripts, deck_list=deck_list, deck_name=deck_name)
148
-
149
-
150
- @database.route("/database/scripts/rename", methods=['POST'])
151
- @login_required
152
- def edit_run_name():
153
- """
154
- .. :quickref: Database; edit workflow name
155
-
156
- edit the name of the current workflow, won't save to the database
157
-
158
- .. http:post:: database/scripts/rename
159
-
160
- : form run_name: new workflow name
161
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
162
-
163
- """
164
- if request.method == "POST":
165
- run_name = request.form.get("run_name")
166
- exist_script = Script.query.get(run_name)
167
- if not exist_script:
168
- script = get_script_file()
169
- script.save_as(run_name)
170
- post_script_file(script)
171
- else:
172
- flash("Script name is already exist in database")
173
- return redirect(url_for("design.experiment_builder"))
174
-
175
-
176
- @database.route("/database/scripts/save_as", methods=['POST'])
177
- @login_required
178
- def save_as():
179
- """
180
- .. :quickref: Database; save the run name as
181
-
182
- save the current workflow script as
183
-
184
- .. http:post:: /database/scripts/save_as
185
-
186
- : form run_name: new workflow name
187
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
188
-
189
- """
190
- if request.method == "POST":
191
- run_name = request.form.get("run_name")
192
- register_workflow = request.form.get("register_workflow")
193
- exist_script = Script.query.get(run_name)
194
- if not exist_script:
195
- script = get_script_file()
196
- script.save_as(run_name)
197
- script.registered = register_workflow == "on"
198
- script.author = session.get('user')
199
- post_script_file(script)
200
- publish()
201
- else:
202
- flash("Script name is already exist in database")
203
- return redirect(url_for("design.experiment_builder"))
204
-
205
-
206
- # -----------------------------------------------------------
207
- # ------------------ Workflow logs -----------------------
208
- # -----------------------------------------------------------
209
- @database.route('/database/workflows/')
210
- def list_workflows():
211
- """
212
- .. :quickref: Database; list all workflow logs
213
-
214
- list all workflow logs
215
-
216
- .. http:get:: /database/workflows/
217
-
218
- """
219
- query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
220
- search_term = request.args.get("keyword", None)
221
- if search_term:
222
- query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
223
- page = request.args.get('page', default=1, type=int)
224
- per_page = 10
225
-
226
- workflows = query.paginate(page=page, per_page=per_page, error_out=False)
227
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
228
- workflows = query.all()
229
- workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
230
- return jsonify({
231
- "workflow_data": workflow_data,
232
- })
233
- else:
234
- return render_template('workflow_database.html', workflows=workflows)
235
-
236
-
237
- @database.route("/database/workflows/<int:workflow_id>")
238
- def get_workflow_steps(workflow_id:int):
239
- """
240
- .. :quickref: Database; list all workflow logs
241
-
242
- list all workflow logs
243
-
244
- .. http:get:: /database/workflows/<int:workflow_id>
245
-
246
- """
247
- workflow = db.session.get(WorkflowRun, workflow_id)
248
- steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
249
-
250
- # Use full objects for template rendering
251
- grouped = {
252
- "prep": [],
253
- "script": {},
254
- "cleanup": [],
255
- }
256
-
257
- # Use dicts for JSON response
258
- grouped_json = {
259
- "prep": [],
260
- "script": {},
261
- "cleanup": [],
262
- }
263
-
264
- for step in steps:
265
- step_dict = step.as_dict()
266
-
267
- if step.phase == "prep":
268
- grouped["prep"].append(step)
269
- grouped_json["prep"].append(step_dict)
270
-
271
- elif step.phase == "script":
272
- grouped["script"].setdefault(step.repeat_index, []).append(step)
273
- grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
274
-
275
- elif step.phase == "cleanup" or step.method_name == "stop":
276
- grouped["cleanup"].append(step)
277
- grouped_json["cleanup"].append(step_dict)
278
-
279
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
280
- return jsonify({
281
- "workflow_info": workflow.as_dict(),
282
- "steps": grouped_json,
283
- })
284
- else:
285
- return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
286
-
287
-
288
- @database.route("/database/workflows/delete/<int:workflow_id>")
289
- @login_required
290
- def delete_workflow_data(workflow_id: int):
291
- """
292
- .. :quickref: Database; delete experiment data from database
293
-
294
- delete workflow data from database
295
-
296
- .. http:get:: /database/workflows/delete/<int:workflow_id>
297
-
298
- :param workflow_id: workflow id
299
- :type workflow_id: int
300
- :status 302: redirect to :http:get:`/ivoryos/database/workflows/`
301
-
302
- """
303
- run = WorkflowRun.query.get(workflow_id)
304
- db.session.delete(run)
305
- db.session.commit()
306
- return redirect(url_for('database.list_workflows'))
@@ -1,7 +0,0 @@
1
- <div class="card mb-2 {{ 'border-danger text-danger bg-light' if step.run_error else 'border-secondary' }}">
2
- <div class="card-body p-2">
3
- <strong>{{ step.method_name }}</strong>
4
- <small>Start: {{ step.start_time }}</small>
5
- <small>End: {{ step.end_time }}</small>
6
- </div>
7
- </div>
@@ -1,130 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}IvoryOS | Experiment Results{% endblock %}
4
-
5
- {% block body %}
6
- <style>
7
- .vis-time-axis .vis-text.vis-minor,
8
- .vis-time-axis .vis-text.vis-major {
9
- color: #666;
10
- }
11
- .vis-item.stop {
12
- background-color: red;
13
- color: white;
14
- border: none;
15
- font-weight: bold;
16
- }
17
- </style>
18
-
19
- <div id="timeline"></div>
20
-
21
- <script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
22
- <link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
23
-
24
- <h1>Experiment Step View</h1>
25
-
26
- <div id="visualization"></div>
27
-
28
- <script type="text/javascript">
29
- var container = document.getElementById('visualization');
30
-
31
- const items = [
32
- {% if grouped.prep %}
33
- {
34
- id: 'prep',
35
- content: 'Prep Phase',
36
- start: '{{ grouped.prep[0].start_time }}',
37
- end: '{{ grouped.prep[-1].end_time }}',
38
- className: 'prep',
39
- group: 'prep'
40
- },
41
- {% endif %}
42
-
43
- {% for repeat_index, step_list in grouped.script.items()|sort %}
44
- {
45
- id: 'iter{{ repeat_index }}',
46
- content: 'Iteration {{ repeat_index }}',
47
- start: '{{ step_list[0].start_time }}',
48
- end: '{{ step_list[-1].end_time }}',
49
- className: 'script',
50
- group: 'iter{{ repeat_index }}'
51
- },
52
- {% for step in step_list %}
53
- {% if step.method_name == "stop" %}
54
- {
55
- id: 'stop-{{ step.id }}',
56
- content: '🛑 Stop',
57
- start: '{{ step.start_time }}',
58
- type: 'point',
59
- className: 'stop',
60
- group: 'iter{{ repeat_index }}'
61
- },
62
- {% endif %}
63
- {% endfor %}
64
- {% endfor %}
65
-
66
- {% if grouped.cleanup %}
67
- {
68
- id: 'cleanup',
69
- content: 'Cleanup Phase',
70
- start: '{{ grouped.cleanup[0].start_time }}',
71
- end: '{{ grouped.cleanup[-1].end_time }}',
72
- className: 'cleanup',
73
- group: 'cleanup'
74
-
75
- },
76
- {% endif %}
77
- ];
78
-
79
- const groups = [
80
- {% if grouped.prep %}{ id: 'prep', content: 'Prep' },{% endif %}
81
- {% for repeat_index in grouped.script.keys()|sort %}{ id: 'iter{{ repeat_index }}', content: 'Iteration {{ repeat_index }}' },{% endfor %}
82
- {% if grouped.cleanup %}{ id: 'cleanup', content: 'Cleanup' },{% endif %}
83
- ];
84
-
85
- var options = {
86
- clickToUse: true,
87
- stack: false, // important to keep point within group row
88
- horizontalScroll: true,
89
- zoomKey: 'ctrlKey'
90
- };
91
-
92
- // Initialize your timeline with the sorted groups
93
- const timeline = new vis.Timeline(container, items, groups, options);
94
-
95
- timeline.on('select', function (props) {
96
- const id = props.items[0];
97
- if (id && id.startsWith('iter')) {
98
- const card = document.getElementById('card-' + id);
99
- if (card) {
100
- const yOffset = -80;
101
- const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
102
- window.scrollTo({ top: y, behavior: 'smooth' });
103
- }
104
- }
105
- });
106
- </script>
107
-
108
- <h2>Workflow: {{ workflow.name }}</h2>
109
-
110
- {% if grouped.prep %}
111
- <h4 class="mt-4">Prep Phase</h4>
112
- {% for step in grouped.prep %}
113
- {% include "step_card.html" %}
114
- {% endfor %}
115
- {% endif %}
116
-
117
- {% for repeat_index, step_list in grouped.script.items()|sort %}
118
- <h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
119
- {% for step in step_list %}
120
- {% include "step_card.html" %}
121
- {% endfor %}
122
- {% endfor %}
123
-
124
- {% if grouped.cleanup %}
125
- <h4 class="mt-4">Cleanup Phase</h4>
126
- {% for step in grouped.cleanup %}
127
- {% include "step_card.html" %}
128
- {% endfor %}
129
- {% endif %}
130
- {% endblock %}