Skip to content

Commit 94ecbcc

Browse files
feat: add ManageAnalysisDialog in gui/components (#293)
* feat: add components/manage_analyses.py and ManageAnalysisDialog` * fix: refer to model.analysis_id
1 parent 1553775 commit 94ecbcc

File tree

4 files changed

+269
-60
lines changed

4 files changed

+269
-60
lines changed

app/analysis_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def display_name(self):
4141

4242
@property
4343
def id(self):
44-
return self.model.id
44+
return self.model.analysis_id
4545

4646
@property
4747
def analyzer_id(self):

gui/components/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
from .analysis import AnalysisParamsCard
2+
from .manage_analyses import ManageAnalysisDialog
23
from .toggle import ToggleButton, ToggleButtonGroup
34
from .upload.upload_button import UploadButton
45

5-
__all__ = ["ToggleButton", "ToggleButtonGroup", "AnalysisParamsCard", "UploadButton"]
6+
__all__ = [
7+
"ToggleButton",
8+
"ToggleButtonGroup",
9+
"AnalysisParamsCard",
10+
"UploadButton",
11+
"ManageAnalysisDialog",
12+
]

gui/components/manage_analyses.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from datetime import datetime
2+
3+
from nicegui import ui
4+
5+
from app.analysis_context import AnalysisContext
6+
from components.select_analysis import present_timestamp
7+
from gui.base import GuiSession
8+
9+
10+
class ManageAnalysisDialog(ui.dialog):
11+
"""
12+
Dialog for managing analyses (view and delete).
13+
14+
Displays a list of analyses for the current project in a grid and allows
15+
users to select and delete one or more analyses.
16+
"""
17+
18+
def __init__(self, session: GuiSession) -> None:
19+
"""
20+
Initialize the Manage Analysis dialog.
21+
22+
Args:
23+
session: GUI session containing app context and state
24+
"""
25+
super().__init__()
26+
27+
self.session = session
28+
now = datetime.now()
29+
self.analysis_contexts: list[AnalysisContext] = (
30+
session.current_project.list_analyses()
31+
if session.current_project is not None
32+
else []
33+
)
34+
# Track IDs of analyses deleted during this dialog session
35+
self.deleted_ids: set = set()
36+
37+
# Build dialog UI
38+
with self, ui.card().classes("w-full"):
39+
# Dialog title
40+
ui.label("Manage Analyses").classes("text-h6 q-mb-md")
41+
42+
# Check if there are analyses to display
43+
if not self.analysis_contexts:
44+
ui.label("No analyses found").classes("text-grey q-mb-md")
45+
else:
46+
# Analyses grid — multiRow selection enabled
47+
self.grid = ui.aggrid(
48+
{
49+
"columnDefs": [
50+
{"headerName": "Analyzer Name", "field": "name"},
51+
{"headerName": "Date Created", "field": "date"},
52+
{"headerName": "ID", "field": "analysis_id", "hide": True},
53+
],
54+
"rowData": [
55+
{
56+
"name": ctx.display_name,
57+
"date": (
58+
present_timestamp(ctx.create_time, now)
59+
if ctx.create_time
60+
else "Unknown"
61+
),
62+
"analysis_id": ctx.id,
63+
}
64+
for ctx in self.analysis_contexts
65+
],
66+
"rowSelection": {"mode": "multiRow"},
67+
},
68+
theme="quartz",
69+
).classes("w-full h-96")
70+
71+
# Action buttons
72+
with ui.row().classes("w-full justify-end gap-2 mt-4"):
73+
ui.button(
74+
"Close",
75+
on_click=self._handle_close,
76+
color="secondary",
77+
).props("outline")
78+
79+
ui.button(
80+
"Delete Selected", on_click=self._handle_delete, color="negative"
81+
)
82+
83+
async def _handle_delete(self) -> None:
84+
"""Handle delete button click — confirm then delete all selected analyses."""
85+
selected_rows = await self.grid.get_selected_rows()
86+
87+
if not selected_rows:
88+
ui.notify("Please select one or more analyses to delete", type="warning")
89+
return
90+
91+
count = len(selected_rows)
92+
93+
# Show a single confirmation for all selected rows
94+
confirmed = await self._show_delete_confirmation(count, selected_rows)
95+
if not confirmed:
96+
return
97+
98+
errors: list[str] = []
99+
newly_deleted: list[str] = []
100+
101+
for row in selected_rows:
102+
analysis_id = row["analysis_id"]
103+
analysis_name = row["name"]
104+
105+
analysis_context = next(
106+
(a for a in self.analysis_contexts if a.id == analysis_id), None
107+
)
108+
109+
if not analysis_context:
110+
errors.append(f"'{analysis_name}' not found")
111+
continue
112+
113+
try:
114+
analysis_context.delete()
115+
if analysis_context.is_deleted:
116+
self.deleted_ids.add(analysis_id)
117+
newly_deleted.append(analysis_id)
118+
except Exception as e:
119+
errors.append(f"'{analysis_name}': {e}")
120+
121+
# Update the dialog grid in place — remove deleted rows
122+
if newly_deleted:
123+
self.grid.options["rowData"] = [
124+
row
125+
for row in self.grid.options["rowData"]
126+
if row["analysis_id"] not in newly_deleted
127+
]
128+
self.grid.update()
129+
130+
if errors:
131+
ui.notify(f"Some deletions failed: {'; '.join(errors)}", type="negative")
132+
elif newly_deleted:
133+
label = "analysis" if len(newly_deleted) == 1 else "analyses"
134+
ui.notify(
135+
f"Deleted {len(newly_deleted)} {label} successfully.", type="positive"
136+
)
137+
138+
async def _show_delete_confirmation(self, count: int, rows: list[dict]) -> bool:
139+
"""
140+
Show confirmation dialog before deleting analyses.
141+
142+
Args:
143+
count: Number of analyses selected for deletion
144+
rows: Selected row data dicts
145+
146+
Returns:
147+
True if user confirmed deletion, False otherwise
148+
"""
149+
if count == 1:
150+
description = f"analysis '{rows[0]['name']}'"
151+
else:
152+
description = f"{count} analyses"
153+
154+
with ui.dialog() as dialog, ui.card():
155+
ui.label(f"Are you sure you want to delete {description}?").classes(
156+
"q-mb-md"
157+
)
158+
ui.label("This action cannot be undone.").classes("text-warning q-mb-lg")
159+
160+
with ui.row().classes("w-full justify-end gap-2"):
161+
ui.button(
162+
"Cancel",
163+
on_click=lambda: dialog.submit(False),
164+
color="secondary",
165+
).props("outline")
166+
167+
ui.button(
168+
"Delete", on_click=lambda: dialog.submit(True), color="negative"
169+
)
170+
171+
return await dialog
172+
173+
def _handle_close(self) -> None:
174+
"""Close the dialog, returning the set of deleted analysis IDs to the caller."""
175+
self.submit(self.deleted_ids)

gui/pages/analyzer_previous.py

Lines changed: 85 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from app.analysis_context import AnalysisContext
66
from components.select_analysis import analysis_label, present_timestamp
77
from gui.base import GuiPage, GuiSession, gui_routes
8+
from gui.components.manage_analyses import ManageAnalysisDialog
89

910

1011
class SelectPreviousAnalyzerPage(GuiPage):
@@ -13,6 +14,7 @@ class SelectPreviousAnalyzerPage(GuiPage):
1314
"""
1415

1516
grid: ui.aggrid | None = None
17+
analysis_contexts: list[AnalysisContext] = []
1618

1719
def __init__(self, session: GuiSession):
1820
select_previous_title: str = "Select Previous Analysis"
@@ -37,6 +39,9 @@ def render_content(self) -> None:
3739
self.navigate_to("/select_project")
3840
return
3941

42+
# Store analyses as instance state so the grid can be updated in place
43+
self.analysis_contexts = self.session.current_project.list_analyses()
44+
4045
# Main content - centered
4146
with (
4247
ui.column()
@@ -45,77 +50,99 @@ def render_content(self) -> None:
4550
):
4651
ui.label("Review a Previous Analysis").classes("text-lg")
4752

48-
# Populate list of existing analyses
49-
now = datetime.now()
50-
analysis_list = sorted(
51-
[
52-
(
53-
analysis_label(analysis, now),
54-
analysis,
55-
)
56-
for analysis in self.session.current_project.list_analyses()
57-
],
58-
key=lambda option: option[0],
59-
)
60-
61-
if analysis_list:
62-
self._render_previous_analyses_grid(entries=analysis_list)
53+
if self.analysis_contexts:
54+
self._render_previous_analyses_grid()
6355
else:
6456
ui.label("No previous tests have been found.").classes("text-grey")
6557

6658
async def _on_proceed():
6759
"""Handle proceed button click."""
68-
# Get selected previous analysis if grid exists
69-
selected_name = None
70-
if analysis_list:
71-
if self.grid is None:
72-
return
73-
74-
selected_rows = await self.grid.get_selected_rows()
75-
if selected_rows:
76-
selected_name = selected_rows[0]["name"]
77-
78-
# Validation: none selected
79-
if not selected_name:
60+
if not self.analysis_contexts:
61+
self.notify_warning("No analyses available")
62+
return
63+
64+
if self.grid is None:
65+
return
66+
67+
selected_rows = await self.grid.get_selected_rows()
68+
if not selected_rows:
8069
self.notify_warning("Please select a previous analysis")
8170
return
8271

83-
else:
84-
self.notify_warning("Coming soon!")
72+
self.notify_warning("Coming soon!")
73+
74+
async def _on_manage_analyses():
75+
"""Handle manage analyses button click."""
76+
dialog = ManageAnalysisDialog(session=self.session)
77+
deleted_ids: set = await dialog
8578

86-
ui.button(
87-
"Proceed",
88-
icon="arrow_forward",
89-
color="primary",
90-
on_click=_on_proceed,
91-
)
79+
if not deleted_ids:
80+
return
9281

93-
def _render_previous_analyses_grid(
94-
self, entries: list[tuple[str, AnalysisContext]]
95-
):
82+
# Remove deleted analyses from instance state
83+
self.analysis_contexts = [
84+
ctx for ctx in self.analysis_contexts if ctx.id not in deleted_ids
85+
]
86+
87+
# Update the page grid in place — no page navigation needed
88+
if self.grid is not None:
89+
now = datetime.now()
90+
self.grid.options["rowData"] = [
91+
{
92+
"name": ctx.display_name,
93+
"date": (
94+
present_timestamp(ctx.create_time, now)
95+
if ctx.create_time
96+
else "Unknown"
97+
),
98+
"analysis_id": ctx.id,
99+
}
100+
for ctx in self.analysis_contexts
101+
]
102+
self.grid.update()
103+
104+
count = len(deleted_ids)
105+
label = "analysis" if count == 1 else "analyses"
106+
self.notify_success(f"Deleted {count} {label}.")
107+
108+
with ui.row().classes("gap-4"):
109+
ui.button(
110+
"Manage Analyses",
111+
icon="settings",
112+
color="secondary",
113+
on_click=_on_manage_analyses,
114+
)
115+
ui.button(
116+
"Proceed",
117+
icon="arrow_forward",
118+
color="primary",
119+
on_click=_on_proceed,
120+
)
121+
122+
def _render_previous_analyses_grid(self) -> None:
96123
"""Render grid of previous analyses."""
97124
now = datetime.now()
98125

99-
data = {
100-
"columnDefs": [
101-
{"headerName": "Analyzer Name", "field": "name"},
102-
{"headerName": "Date Created", "field": "date"},
103-
],
104-
"rowData": [
105-
{
106-
"name": analysis_context.display_name,
107-
"date": (
108-
present_timestamp(analysis_context.create_time, now)
109-
if analysis_context.create_time
110-
else "Unknown"
111-
),
112-
}
113-
for label, analysis_context in entries
114-
],
115-
"rowSelection": {"mode": "singleRow"},
116-
}
117-
118126
self.grid = ui.aggrid(
119-
data,
127+
{
128+
"columnDefs": [
129+
{"headerName": "Analyzer Name", "field": "name"},
130+
{"headerName": "Date Created", "field": "date"},
131+
{"headerName": "ID", "field": "analysis_id", "hide": True},
132+
],
133+
"rowData": [
134+
{
135+
"name": ctx.display_name,
136+
"date": (
137+
present_timestamp(ctx.create_time, now)
138+
if ctx.create_time
139+
else "Unknown"
140+
),
141+
"analysis_id": ctx.id,
142+
}
143+
for ctx in self.analysis_contexts
144+
],
145+
"rowSelection": {"mode": "singleRow"},
146+
},
120147
theme="quartz",
121148
).classes("w-full h-64")

0 commit comments

Comments
 (0)