ivoryos 1.2.8__tar.gz → 1.3.0__tar.gz
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.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- {ivoryos-1.2.8 → ivoryos-1.3.0}/PKG-INFO +1 -1
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/app.py +38 -1
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/api/api.py +2 -1
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/templates/controllers.html +9 -0
- ivoryos-1.3.0/ivoryos/routes/data/data.py +170 -0
- ivoryos-1.3.0/ivoryos/routes/data/templates/components/step_card.html +42 -0
- ivoryos-1.3.0/ivoryos/routes/data/templates/workflow_view.html +351 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/db_models.py +44 -8
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/form.py +4 -2
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/script_runner.py +103 -48
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/utils.py +6 -3
- ivoryos-1.3.0/ivoryos/version.py +1 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos.egg-info/PKG-INFO +1 -1
- ivoryos-1.2.8/ivoryos/routes/data/data.py +0 -131
- ivoryos-1.2.8/ivoryos/routes/data/templates/components/step_card.html +0 -13
- ivoryos-1.2.8/ivoryos/routes/data/templates/workflow_view.html +0 -130
- ivoryos-1.2.8/ivoryos/version.py +0 -1
- {ivoryos-1.2.8 → ivoryos-1.3.0}/LICENSE +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/README.md +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/config.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/optimizer/ax_optimizer.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/optimizer/base_optimizer.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/optimizer/baybe_optimizer.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/optimizer/registry.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/auth/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/auth/auth.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/auth/templates/login.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/auth/templates/signup.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/control.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/control_file.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/control_new_device.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/templates/controllers_new.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/control/utils.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/data/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/data/templates/workflow_database.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/design.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/design_file.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/design_step.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/action_form.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/execute.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/execute_file.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/library/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/library/library.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/library/templates/library.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/main/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/main/main.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/main/templates/help.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/routes/main/templates/home.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/server.py +1 -1
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/socket_handlers.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/favicon.ico +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/gui_annotation/Slide1.png +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/action_handlers.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/db_delete.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/overlay.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/script_metadata.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/socket_handler.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/sortable_card.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/sortable_design.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/js/ui_state.js +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/logo.webp +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/static/style.css +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/templates/base.html +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/__init__.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/bo_campaign.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/client_proxy.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/decorators.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/global_config.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/llm_agent.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/py_to_json.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/serilize.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos/utils/task_runner.py +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos.egg-info/SOURCES.txt +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos.egg-info/dependency_links.txt +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos.egg-info/requires.txt +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/ivoryos.egg-info/top_level.txt +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/pyproject.toml +0 -0
- {ivoryos-1.2.8 → ivoryos-1.3.0}/setup.cfg +0 -0
|
@@ -17,6 +17,42 @@ from ivoryos.routes.api.api import api
|
|
|
17
17
|
from ivoryos.socket_handlers import socketio
|
|
18
18
|
from ivoryos.routes.main.main import main
|
|
19
19
|
from ivoryos.version import __version__ as ivoryos_version
|
|
20
|
+
from sqlalchemy import inspect, text
|
|
21
|
+
from flask import current_app
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def reset_old_schema(engine, db_dir):
|
|
25
|
+
inspector = inspect(engine)
|
|
26
|
+
tables = inspector.get_table_names()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Check if old tables exist (no workflow_phases table)
|
|
30
|
+
has_workflow_phase = 'workflow_phases' in tables
|
|
31
|
+
old_workflow_run = 'old_workflow_run' in tables
|
|
32
|
+
old_workflow_step = 'workflow_steps' in tables
|
|
33
|
+
|
|
34
|
+
if not has_workflow_phase:
|
|
35
|
+
print("⚠️ Old workflow database detected! All previous workflows have been reset to support the new schema.")
|
|
36
|
+
# Backup old DB
|
|
37
|
+
db_path = os.path.join(db_dir, "ivoryos.db")
|
|
38
|
+
if os.path.exists(db_path):
|
|
39
|
+
# os.makedirs(backup_dir, exist_ok=True)
|
|
40
|
+
from datetime import datetime
|
|
41
|
+
import shutil
|
|
42
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
43
|
+
backup_path = os.path.join(db_dir, f"ivoryos_backup_{ts}.db")
|
|
44
|
+
shutil.copy(db_path, backup_path)
|
|
45
|
+
print(f"Backup created at {backup_path}")
|
|
46
|
+
with engine.begin() as conn:
|
|
47
|
+
# Drop old tables
|
|
48
|
+
if old_workflow_step:
|
|
49
|
+
conn.execute(text("DROP TABLE IF EXISTS workflow_steps"))
|
|
50
|
+
if old_workflow_run:
|
|
51
|
+
conn.execute(text("DROP TABLE IF EXISTS workflow_runs"))
|
|
52
|
+
|
|
53
|
+
# Recreate new schema
|
|
54
|
+
db.create_all() # creates workflow_runs, workflow_phases, workflow_steps
|
|
55
|
+
|
|
20
56
|
|
|
21
57
|
def create_app(config_class=None):
|
|
22
58
|
"""
|
|
@@ -45,7 +81,8 @@ def create_app(config_class=None):
|
|
|
45
81
|
|
|
46
82
|
# Create database tables
|
|
47
83
|
with app.app_context():
|
|
48
|
-
db.create_all()
|
|
84
|
+
# db.create_all()
|
|
85
|
+
reset_old_schema(db.engine, app.config['OUTPUT_FOLDER'])
|
|
49
86
|
|
|
50
87
|
# Additional setup
|
|
51
88
|
utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import os
|
|
2
3
|
from flask import Blueprint, jsonify, request, current_app
|
|
3
4
|
|
|
@@ -46,7 +47,7 @@ def backend_control(instrument: str=None):
|
|
|
46
47
|
current_app=current_app._get_current_object())
|
|
47
48
|
return jsonify(output), 200
|
|
48
49
|
|
|
49
|
-
snapshot = global_config.deck_snapshot
|
|
50
|
+
snapshot = copy.deepcopy(global_config.deck_snapshot)
|
|
50
51
|
# Iterate through each instrument in the snapshot
|
|
51
52
|
for instrument_key, instrument_data in snapshot.items():
|
|
52
53
|
# Iterate through each function associated with the current instrument
|
|
@@ -107,6 +107,15 @@
|
|
|
107
107
|
{{ field(class="btn btn-dark") }}
|
|
108
108
|
{% elif field.type == "BooleanField" %}
|
|
109
109
|
{{ field(class="form-check-input") }}
|
|
110
|
+
{% elif field.type == "FlexibleEnumField" %}
|
|
111
|
+
<input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
|
|
112
|
+
list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
|
|
113
|
+
class="form-control">
|
|
114
|
+
<datalist id="{{ field.id }}_options">
|
|
115
|
+
{% for key in field.choices %}
|
|
116
|
+
<option value="{{ key }}">{{ key }}</option>
|
|
117
|
+
{% endfor %}
|
|
118
|
+
</datalist>
|
|
110
119
|
{% else %}
|
|
111
120
|
{{ field(class="form-control") }}
|
|
112
121
|
{% endif %}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
|
|
4
|
+
from flask_login import login_required
|
|
5
|
+
|
|
6
|
+
from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep, WorkflowPhase
|
|
7
|
+
|
|
8
|
+
data = Blueprint('data', __name__, template_folder='templates')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@data.route('/executions/records')
|
|
13
|
+
@login_required
|
|
14
|
+
def list_workflows():
|
|
15
|
+
"""
|
|
16
|
+
.. :quickref: Workflow Execution Database; list all workflow execution records
|
|
17
|
+
|
|
18
|
+
list all workflow execution records
|
|
19
|
+
|
|
20
|
+
.. http:get:: /executions/records
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
|
|
24
|
+
search_term = request.args.get("keyword", None)
|
|
25
|
+
if search_term:
|
|
26
|
+
query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
|
|
27
|
+
page = request.args.get('page', default=1, type=int)
|
|
28
|
+
per_page = 10
|
|
29
|
+
|
|
30
|
+
workflows = query.paginate(page=page, per_page=per_page, error_out=False)
|
|
31
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
32
|
+
workflows = query.all()
|
|
33
|
+
workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
|
|
34
|
+
return jsonify({
|
|
35
|
+
"workflow_data": workflow_data,
|
|
36
|
+
})
|
|
37
|
+
else:
|
|
38
|
+
return render_template('workflow_database.html', workflows=workflows)
|
|
39
|
+
|
|
40
|
+
@data.get("/executions/records/<int:workflow_id>")
|
|
41
|
+
def workflow_logs(workflow_id:int):
|
|
42
|
+
"""
|
|
43
|
+
.. :quickref: Workflow Data Database; get workflow data, steps, and logs
|
|
44
|
+
|
|
45
|
+
get workflow data logs by workflow id
|
|
46
|
+
|
|
47
|
+
.. http:get:: /executions/<int:workflow_id>
|
|
48
|
+
|
|
49
|
+
:param workflow_id: workflow id
|
|
50
|
+
:type workflow_id: int
|
|
51
|
+
"""
|
|
52
|
+
workflow = db.session.get(WorkflowRun, workflow_id)
|
|
53
|
+
if not workflow:
|
|
54
|
+
return jsonify({"error": "Workflow not found"}), 404
|
|
55
|
+
|
|
56
|
+
# Query all phases for this run, ordered by start_time
|
|
57
|
+
phases = WorkflowPhase.query.filter_by(run_id=workflow_id).order_by(WorkflowPhase.start_time).all()
|
|
58
|
+
|
|
59
|
+
# Prepare grouped data for template (full objects)
|
|
60
|
+
grouped = {
|
|
61
|
+
"prep": [],
|
|
62
|
+
"script": {},
|
|
63
|
+
"cleanup": [],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Prepare grouped data for JSON (dicts)
|
|
67
|
+
grouped_json = {
|
|
68
|
+
"prep": [],
|
|
69
|
+
"script": {},
|
|
70
|
+
"cleanup": [],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for phase in phases:
|
|
74
|
+
phase_dict = phase.as_dict()
|
|
75
|
+
|
|
76
|
+
# Steps sorted by step_index
|
|
77
|
+
steps = sorted(phase.steps, key=lambda s: s.step_index)
|
|
78
|
+
phase_steps_dicts = [s.as_dict() for s in steps]
|
|
79
|
+
|
|
80
|
+
if phase.name == "prep":
|
|
81
|
+
grouped["prep"].append(phase)
|
|
82
|
+
grouped_json["prep"].append({
|
|
83
|
+
**phase_dict,
|
|
84
|
+
"steps": phase_steps_dicts
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
elif phase.name == "main":
|
|
88
|
+
grouped["script"].setdefault(phase.repeat_index, []).append(phase)
|
|
89
|
+
grouped_json["script"].setdefault(phase.repeat_index, []).append({
|
|
90
|
+
**phase_dict,
|
|
91
|
+
"steps": phase_steps_dicts
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
elif phase.name == "cleanup":
|
|
95
|
+
grouped["cleanup"].append(phase)
|
|
96
|
+
grouped_json["cleanup"].append({
|
|
97
|
+
**phase_dict,
|
|
98
|
+
"steps": phase_steps_dicts
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
102
|
+
return jsonify({
|
|
103
|
+
"workflow_info": workflow.as_dict(),
|
|
104
|
+
"phases": grouped_json,
|
|
105
|
+
})
|
|
106
|
+
else:
|
|
107
|
+
return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@data.get("/executions/data/<int:workflow_id>")
|
|
111
|
+
def workflow_phase_data(workflow_id: int):
|
|
112
|
+
workflow = db.session.get(WorkflowRun, workflow_id)
|
|
113
|
+
if not workflow:
|
|
114
|
+
return jsonify({})
|
|
115
|
+
|
|
116
|
+
phase_data = {}
|
|
117
|
+
# Only plot 'main' phases
|
|
118
|
+
main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main').order_by(
|
|
119
|
+
WorkflowPhase.repeat_index).all()
|
|
120
|
+
|
|
121
|
+
for phase in main_phases:
|
|
122
|
+
outputs = phase.outputs or {}
|
|
123
|
+
phase_index = phase.repeat_index
|
|
124
|
+
# Convert each key to list of dicts for x (phase_index) and y (value)
|
|
125
|
+
phase_data[phase_index] = {}
|
|
126
|
+
for k, v in outputs.items():
|
|
127
|
+
if isinstance(v, (int, float)):
|
|
128
|
+
phase_data[phase_index][k] = [{"x": phase_index, "y": v}]
|
|
129
|
+
elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
|
|
130
|
+
phase_data[phase_index][k] = v.map(lambda val, idx=0: {"x": phase_index, "y": val})
|
|
131
|
+
|
|
132
|
+
return jsonify(phase_data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@data.delete("/executions/records/<int:workflow_id>")
|
|
136
|
+
@login_required
|
|
137
|
+
def delete_workflow_record(workflow_id: int):
|
|
138
|
+
"""
|
|
139
|
+
.. :quickref: Workflow Data Database; delete a workflow execution record
|
|
140
|
+
|
|
141
|
+
delete a workflow execution record by workflow id
|
|
142
|
+
|
|
143
|
+
.. http:delete:: /executions/records/<int:workflow_id>
|
|
144
|
+
|
|
145
|
+
:param workflow_id: workflow id
|
|
146
|
+
:type workflow_id: int
|
|
147
|
+
:status 200: return success message
|
|
148
|
+
"""
|
|
149
|
+
run = WorkflowRun.query.get(workflow_id)
|
|
150
|
+
db.session.delete(run)
|
|
151
|
+
db.session.commit()
|
|
152
|
+
return jsonify(success=True)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@data.route('/files/execution-data/<string:filename>')
|
|
156
|
+
@login_required
|
|
157
|
+
def download_results(filename:str):
|
|
158
|
+
"""
|
|
159
|
+
.. :quickref: Workflow data; download a workflow data file (.CSV)
|
|
160
|
+
|
|
161
|
+
.. http:get:: /files/execution-data/<string:filename>
|
|
162
|
+
|
|
163
|
+
:param filename: workflow data filename
|
|
164
|
+
:type filename: str
|
|
165
|
+
|
|
166
|
+
# :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions`
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
|
|
170
|
+
return send_file(os.path.abspath(filepath), as_attachment=True)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<div class="card mb-2 {{ 'border-danger text-danger bg-light' if phase.run_error else 'border-secondary' }}">
|
|
2
|
+
<div class="card-body p-2">
|
|
3
|
+
<small class="text-muted">
|
|
4
|
+
<i class="fas fa-play-circle me-1"></i> Start: {{ phase.start_time.strftime('%H:%M:%S') if phase.start_time else 'N/A' }}
|
|
5
|
+
<i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ phase.end_time.strftime('%H:%M:%S') if phase.end_time else 'N/A' }}
|
|
6
|
+
</small>
|
|
7
|
+
{% if phase.parameters %}
|
|
8
|
+
<div class="mt-2">
|
|
9
|
+
<strong>Parameters: </strong>
|
|
10
|
+
{% for key, value in phase.parameters.items() %}
|
|
11
|
+
<span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
|
|
12
|
+
{% endfor %}
|
|
13
|
+
</div>
|
|
14
|
+
{% endif %}
|
|
15
|
+
{% if phase.steps %}
|
|
16
|
+
<div class="mt-2">
|
|
17
|
+
<strong>Steps:</strong>
|
|
18
|
+
<ul class="mb-0">
|
|
19
|
+
{% for step in phase.steps %}
|
|
20
|
+
<li class="{{ 'text-danger' if step.run_error else '' }}">
|
|
21
|
+
{{ step.method_name }}
|
|
22
|
+
<small class="text-muted">
|
|
23
|
+
({{ step.start_time.strftime('%H:%M:%S') if step.start_time else 'N/A' }} –
|
|
24
|
+
{{ step.end_time.strftime('%H:%M:%S') if step.end_time else 'N/A' }})
|
|
25
|
+
</small>
|
|
26
|
+
</li>
|
|
27
|
+
{% endfor %}
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
30
|
+
{% endif %}
|
|
31
|
+
{% if phase.outputs %}
|
|
32
|
+
<div class="mt-1">
|
|
33
|
+
<strong>Outputs:</strong>
|
|
34
|
+
<ul class="mb-0">
|
|
35
|
+
{% for key, value in phase.outputs.items() %}
|
|
36
|
+
<li>{{ key }}: {{ value }}</li>
|
|
37
|
+
{% endfor %}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
{% endif %}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,351 @@
|
|
|
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: #dc3545;
|
|
13
|
+
color: white;
|
|
14
|
+
border: none;
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
}
|
|
17
|
+
.vis-item.prep {
|
|
18
|
+
background-color: #17a2b8;
|
|
19
|
+
border-color: #138496;
|
|
20
|
+
}
|
|
21
|
+
.vis-item.script {
|
|
22
|
+
background-color: #28a745;
|
|
23
|
+
border-color: #1e7e34;
|
|
24
|
+
}
|
|
25
|
+
.vis-item.cleanup {
|
|
26
|
+
background-color: #ffc107;
|
|
27
|
+
border-color: #d39e00;
|
|
28
|
+
color: #212529;
|
|
29
|
+
}
|
|
30
|
+
#visualization {
|
|
31
|
+
border: 1px solid #dee2e6;
|
|
32
|
+
border-radius: 0.375rem;
|
|
33
|
+
background-color: #fff;
|
|
34
|
+
min-height: 200px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.section-header {
|
|
38
|
+
border-bottom: 2px solid #dee2e6;
|
|
39
|
+
padding-bottom: 0.5rem;
|
|
40
|
+
margin-bottom: 1rem;
|
|
41
|
+
color: #495057;
|
|
42
|
+
}
|
|
43
|
+
.loading-spinner {
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
<div class="timeline-section" style="margin-bottom: 2rem;">
|
|
50
|
+
<h3 class="section-header">
|
|
51
|
+
<i class="fas fa-clock me-2"></i>Execution Timeline
|
|
52
|
+
</h3>
|
|
53
|
+
<div class="alert alert-info" role="alert">
|
|
54
|
+
<i class="fas fa-info-circle me-2"></i>
|
|
55
|
+
<strong>Tip:</strong> Click on timeline items to navigate to detailed views. Use Ctrl+scroll to zoom.
|
|
56
|
+
</div>
|
|
57
|
+
<div id="visualization"></div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Phase Output Plot Section -->
|
|
61
|
+
|
|
62
|
+
<div class="data-section" style="margin-bottom: 2rem;">
|
|
63
|
+
<h3 class="section-header">
|
|
64
|
+
<div class="col-md-6">
|
|
65
|
+
<label for="output-select" class="form-label">Select Data Type:</label>
|
|
66
|
+
<select id="output-select" class="form-select">
|
|
67
|
+
<option value="">Loading data types...</option>
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
</h3>
|
|
71
|
+
|
|
72
|
+
<div class="plot-controls">
|
|
73
|
+
<div>
|
|
74
|
+
<div class="col-md-6 text-md-end">
|
|
75
|
+
<div class="loading-spinner">
|
|
76
|
+
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
|
|
77
|
+
<span class="visually-hidden">Loading...</span>
|
|
78
|
+
</div>
|
|
79
|
+
Loading plot data...
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div id="phase-plot" style="height:450px;" class="border rounded bg-white shadow-sm"></div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Workflow Details Section -->
|
|
88
|
+
|
|
89
|
+
<div>
|
|
90
|
+
<h2 class="section-header">
|
|
91
|
+
<i class="fas fa-project-diagram me-2"></i>Workflow: {{ workflow.name }}
|
|
92
|
+
</h2>
|
|
93
|
+
|
|
94
|
+
<!-- Prep Phase -->
|
|
95
|
+
{% if grouped.prep %}
|
|
96
|
+
<div class="mb-4">
|
|
97
|
+
<h4 class="text-info mb-3">
|
|
98
|
+
<i class="fas fa-tools me-2"></i>Prep
|
|
99
|
+
</h4>
|
|
100
|
+
<div class="row">
|
|
101
|
+
{% for phase in grouped.prep %}
|
|
102
|
+
<div class="col-lg-6 mb-3">
|
|
103
|
+
{% include "components/step_card.html" %}
|
|
104
|
+
</div>
|
|
105
|
+
{% endfor %}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{% endif %}
|
|
109
|
+
|
|
110
|
+
<!-- Script Iterations -->
|
|
111
|
+
{% for repeat_index, phase_list in grouped.script.items()|sort %}
|
|
112
|
+
<div class="mb-4" id="card-iter{{ repeat_index }}">
|
|
113
|
+
<h4 class="text-success mb-3">
|
|
114
|
+
<i class="fas fa-redo me-2"></i>Iteration {{ repeat_index }}
|
|
115
|
+
</h4>
|
|
116
|
+
<div class="row">
|
|
117
|
+
{% for phase in phase_list %}
|
|
118
|
+
<div class="col-lg-6 mb-3">
|
|
119
|
+
{% include "components/step_card.html" %}
|
|
120
|
+
</div>
|
|
121
|
+
{% endfor %}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
{% endfor %}
|
|
125
|
+
|
|
126
|
+
<!-- Cleanup Phase -->
|
|
127
|
+
{% if grouped.cleanup %}
|
|
128
|
+
<div>
|
|
129
|
+
<h4 class="text-warning mb-3">
|
|
130
|
+
<i class="fas fa-broom me-2"></i>Cleanup
|
|
131
|
+
</h4>
|
|
132
|
+
<div class="row">
|
|
133
|
+
{% for phase in grouped.cleanup %}
|
|
134
|
+
<div class="col-lg-6 mb-3">
|
|
135
|
+
{% include "components/step_card.html" %}
|
|
136
|
+
</div>
|
|
137
|
+
{% endfor %}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{% endif %}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
<!-- External Dependencies -->
|
|
146
|
+
<script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
|
147
|
+
<link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
|
|
148
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
149
|
+
|
|
150
|
+
<script type="text/javascript">
|
|
151
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
152
|
+
// ---------------- Timeline Setup ----------------
|
|
153
|
+
const container = document.getElementById('visualization');
|
|
154
|
+
const groups = [
|
|
155
|
+
{ id: 'all', content: 'Workflow Execution' }
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const items = [
|
|
159
|
+
{% if grouped.prep %}
|
|
160
|
+
{
|
|
161
|
+
id: 'prep',
|
|
162
|
+
content: 'Prep',
|
|
163
|
+
start: '{{ grouped.prep[0].start_time }}',
|
|
164
|
+
end: '{{ grouped.prep[-1].end_time }}',
|
|
165
|
+
className: 'prep',
|
|
166
|
+
group: 'all',
|
|
167
|
+
},
|
|
168
|
+
{% endif %}
|
|
169
|
+
|
|
170
|
+
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
171
|
+
{
|
|
172
|
+
id: 'iter{{ repeat_index }}',
|
|
173
|
+
content: 'Iteration {{ repeat_index }}',
|
|
174
|
+
start: '{{ step_list[0].start_time }}',
|
|
175
|
+
end: '{{ step_list[-1].end_time }}',
|
|
176
|
+
className: 'script',
|
|
177
|
+
group: 'all',
|
|
178
|
+
},
|
|
179
|
+
{% for step in step_list %}
|
|
180
|
+
{% if step.method_name == "stop" %}
|
|
181
|
+
{
|
|
182
|
+
id: 'stop-{{ step.id }}',
|
|
183
|
+
content: '🛑 Stop',
|
|
184
|
+
start: '{{ step.start_time }}',
|
|
185
|
+
type: 'point',
|
|
186
|
+
className: 'stop',
|
|
187
|
+
group: 'all',
|
|
188
|
+
title: 'Stop event at {{ step.start_time }}'
|
|
189
|
+
},
|
|
190
|
+
{% endif %}
|
|
191
|
+
{% endfor %}
|
|
192
|
+
{% endfor %}
|
|
193
|
+
|
|
194
|
+
{% if grouped.cleanup %}
|
|
195
|
+
{
|
|
196
|
+
id: 'cleanup',
|
|
197
|
+
content: 'Cleanup ',
|
|
198
|
+
start: '{{ grouped.cleanup[0].start_time }}',
|
|
199
|
+
end: '{{ grouped.cleanup[-1].end_time }}',
|
|
200
|
+
className: 'cleanup',
|
|
201
|
+
group: 'all',
|
|
202
|
+
},
|
|
203
|
+
{% endif %}
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
var timeline = new vis.Timeline(container, items, groups, {
|
|
207
|
+
clickToUse: true,
|
|
208
|
+
stack: false, // keep items from overlapping vertically
|
|
209
|
+
horizontalScroll: true,
|
|
210
|
+
zoomKey: 'ctrlKey'
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
timeline.on('select', function (props) {
|
|
214
|
+
const id = props.items[0];
|
|
215
|
+
if (id && id.startsWith('iter')) {
|
|
216
|
+
const card = document.getElementById('card-' + id);
|
|
217
|
+
if (card) {
|
|
218
|
+
const yOffset = -80;
|
|
219
|
+
const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
|
220
|
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------- Phase Data Plot ----------------
|
|
226
|
+
const loadingSpinner = document.querySelector('.loading-spinner');
|
|
227
|
+
const select = document.getElementById('output-select');
|
|
228
|
+
|
|
229
|
+
loadingSpinner.style.display = 'block';
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
fetch("{{ url_for('data.workflow_phase_data', workflow_id=workflow.id) }}")
|
|
233
|
+
.then(res => {
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
236
|
+
}
|
|
237
|
+
return res.json();
|
|
238
|
+
})
|
|
239
|
+
.then(data => {
|
|
240
|
+
loadingSpinner.style.display = 'none';
|
|
241
|
+
|
|
242
|
+
const repeatKeys = Object.keys(data).sort((a, b) => a - b);
|
|
243
|
+
const dataSection = document.querySelector('.data-section'); // Get the entire section
|
|
244
|
+
|
|
245
|
+
if (!repeatKeys.length) {
|
|
246
|
+
// Hide the entire data section if no data
|
|
247
|
+
dataSection.style.display = 'none';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const allKeys = new Set();
|
|
252
|
+
repeatKeys.forEach(k => {
|
|
253
|
+
Object.keys(data[k]).forEach(key => allKeys.add(key));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// If no keys found, also hide the section
|
|
257
|
+
if (allKeys.size === 0) {
|
|
258
|
+
dataSection.style.display = 'none';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Show the data section since we have data
|
|
263
|
+
dataSection.style.display = 'block';
|
|
264
|
+
|
|
265
|
+
// Clear and populate select options
|
|
266
|
+
select.innerHTML = '';
|
|
267
|
+
allKeys.forEach(k => {
|
|
268
|
+
const option = new Option(k, k);
|
|
269
|
+
select.appendChild(option);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function plotData(selectedKey) {
|
|
273
|
+
const x = [];
|
|
274
|
+
const y = [];
|
|
275
|
+
|
|
276
|
+
repeatKeys.forEach(repeat_index => {
|
|
277
|
+
const arr = data[repeat_index][selectedKey];
|
|
278
|
+
if (arr && arr.length) {
|
|
279
|
+
arr.forEach(d => {
|
|
280
|
+
if (typeof d === 'object' && d.x !== undefined && d.y !== undefined) {
|
|
281
|
+
x.push(d.x);
|
|
282
|
+
y.push(d.y);
|
|
283
|
+
} else if (typeof d === 'number') {
|
|
284
|
+
x.push(parseInt(repeat_index));
|
|
285
|
+
y.push(d);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const trace = {
|
|
292
|
+
x: x,
|
|
293
|
+
y: y,
|
|
294
|
+
mode: 'markers',
|
|
295
|
+
name: selectedKey,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const layout = {
|
|
299
|
+
xaxis: {
|
|
300
|
+
title: 'Iteration Index',
|
|
301
|
+
gridcolor: '#e9ecef'
|
|
302
|
+
},
|
|
303
|
+
yaxis: {
|
|
304
|
+
title: selectedKey,
|
|
305
|
+
gridcolor: '#e9ecef'
|
|
306
|
+
},
|
|
307
|
+
plot_bgcolor: '#ffffff',
|
|
308
|
+
paper_bgcolor: '#ffffff',
|
|
309
|
+
margin: { t: 60, r: 40, b: 60, l: 80 }
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const config = {
|
|
313
|
+
responsive: true,
|
|
314
|
+
displayModeBar: true,
|
|
315
|
+
modeBarButtonsToRemove: ['lasso2d', 'select2d']
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
Plotly.newPlot('phase-plot', [trace], layout, config);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
select.addEventListener('change', e => {
|
|
322
|
+
if (e.target.value) {
|
|
323
|
+
plotData(e.target.value);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Plot first available data type
|
|
328
|
+
if (allKeys.size > 0) {
|
|
329
|
+
plotData([...allKeys][0]);
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.catch(error => {
|
|
333
|
+
loadingSpinner.style.display = 'none';
|
|
334
|
+
console.error('Error loading phase data:', error);
|
|
335
|
+
|
|
336
|
+
const dataSection = document.querySelector('.data-section');
|
|
337
|
+
// Hide the section on error as well
|
|
338
|
+
dataSection.style.display = 'none';
|
|
339
|
+
|
|
340
|
+
// Optionally, you could show an error message instead:
|
|
341
|
+
// dataSection.innerHTML = `
|
|
342
|
+
// <div class="alert alert-danger m-3" role="alert">
|
|
343
|
+
// <i class="fas fa-exclamation-triangle me-2"></i>
|
|
344
|
+
// <strong>Error:</strong> Unable to load phase data. ${error.message}
|
|
345
|
+
// </div>
|
|
346
|
+
// `;
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
</script>
|
|
350
|
+
|
|
351
|
+
{% endblock %}
|