reasoning-deployment-service 0.2.8__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.
Potentially problematic release.
This version of reasoning-deployment-service might be problematic. Click here for more details.
- examples/programmatic_usage.py +154 -0
- reasoning_deployment_service/__init__.py +25 -0
- reasoning_deployment_service/cli_editor/__init__.py +5 -0
- reasoning_deployment_service/cli_editor/api_client.py +666 -0
- reasoning_deployment_service/cli_editor/cli_runner.py +343 -0
- reasoning_deployment_service/cli_editor/config.py +82 -0
- reasoning_deployment_service/cli_editor/google_deps.py +29 -0
- reasoning_deployment_service/cli_editor/reasoning_engine_creator.py +448 -0
- reasoning_deployment_service/gui_editor/__init__.py +5 -0
- reasoning_deployment_service/gui_editor/main.py +280 -0
- reasoning_deployment_service/gui_editor/requirements_minimal.txt +54 -0
- reasoning_deployment_service/gui_editor/run_program.sh +55 -0
- reasoning_deployment_service/gui_editor/src/__init__.py +1 -0
- reasoning_deployment_service/gui_editor/src/core/__init__.py +1 -0
- reasoning_deployment_service/gui_editor/src/core/api_client.py +647 -0
- reasoning_deployment_service/gui_editor/src/core/config.py +43 -0
- reasoning_deployment_service/gui_editor/src/core/google_deps.py +22 -0
- reasoning_deployment_service/gui_editor/src/core/reasoning_engine_creator.py +448 -0
- reasoning_deployment_service/gui_editor/src/ui/__init__.py +1 -0
- reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py +312 -0
- reasoning_deployment_service/gui_editor/src/ui/authorization_view.py +280 -0
- reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py +354 -0
- reasoning_deployment_service/gui_editor/src/ui/reasoning_engines_view.py +204 -0
- reasoning_deployment_service/gui_editor/src/ui/ui_components.py +1221 -0
- reasoning_deployment_service/reasoning_deployment_service.py +687 -0
- reasoning_deployment_service-0.2.8.dist-info/METADATA +177 -0
- reasoning_deployment_service-0.2.8.dist-info/RECORD +29 -0
- reasoning_deployment_service-0.2.8.dist-info/WHEEL +5 -0
- reasoning_deployment_service-0.2.8.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Agent Space management view."""
|
|
2
|
+
import time
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from tkinter import ttk, messagebox
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from ..core.api_client import ApiClient
|
|
8
|
+
from .ui_components import async_operation, StatusButton, AgentDetailsDialog, LoadingDialog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentSpaceView(ttk.Frame):
|
|
12
|
+
"""UI for listing and deleting Agent Space agents."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, master, api: ApiClient, log: Callable[[str], None]):
|
|
15
|
+
super().__init__(master)
|
|
16
|
+
self.api = api
|
|
17
|
+
self.log = log
|
|
18
|
+
|
|
19
|
+
# Cache authentication state to avoid repeated API calls
|
|
20
|
+
self._cached_auth_state = None
|
|
21
|
+
self._last_auth_check = 0
|
|
22
|
+
self._auth_cache_duration = 30 # 30 seconds
|
|
23
|
+
|
|
24
|
+
self._setup_ui()
|
|
25
|
+
|
|
26
|
+
def _setup_ui(self):
|
|
27
|
+
# Control buttons
|
|
28
|
+
btns = ttk.Frame(self)
|
|
29
|
+
btns.pack(fill="x", pady=(6, 4))
|
|
30
|
+
|
|
31
|
+
self.refresh_btn = StatusButton(btns, text="Refresh Agents", command=self.refresh)
|
|
32
|
+
self.refresh_btn.pack(side="left", padx=4)
|
|
33
|
+
|
|
34
|
+
self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
|
|
35
|
+
self.delete_btn.pack(side="left", padx=8)
|
|
36
|
+
|
|
37
|
+
self.details_btn = StatusButton(btns, text="More Agent Details", command=self.show_agent_details)
|
|
38
|
+
self.details_btn.pack(side="left", padx=8)
|
|
39
|
+
|
|
40
|
+
self.status = tk.StringVar(value="Ready.")
|
|
41
|
+
ttk.Label(btns, textvariable=self.status).pack(side="right")
|
|
42
|
+
|
|
43
|
+
# Add dropdown for environment selection
|
|
44
|
+
self.env_var = tk.StringVar(value="dev")
|
|
45
|
+
ttk.Label(btns, text="Environment:").pack(side="left", padx=(10, 0))
|
|
46
|
+
ttk.OptionMenu(btns, self.env_var, "dev", "prod").pack(side="left", padx=(0, 10))
|
|
47
|
+
|
|
48
|
+
# Agent list
|
|
49
|
+
wrap = ttk.Frame(self)
|
|
50
|
+
wrap.pack(fill="both", expand=True)
|
|
51
|
+
cols = ("id", "display_name", "authorization_id", "engine_id")
|
|
52
|
+
self.tree = ttk.Treeview(wrap, columns=cols, show="headings", selectmode="extended")
|
|
53
|
+
|
|
54
|
+
for c, t, w in [
|
|
55
|
+
("id", "Agent ID", 250),
|
|
56
|
+
("display_name", "Display Name", 250),
|
|
57
|
+
("authorization_id", "Authorization", 250),
|
|
58
|
+
("engine_id", "Engine", 250),
|
|
59
|
+
]:
|
|
60
|
+
self.tree.heading(c, text=t)
|
|
61
|
+
self.tree.column(c, width=w, anchor="w")
|
|
62
|
+
|
|
63
|
+
self.tree.pack(side="left", fill="both", expand=True)
|
|
64
|
+
vsb = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
|
|
65
|
+
self.tree.configure(yscroll=vsb.set)
|
|
66
|
+
vsb.pack(side="right", fill="y")
|
|
67
|
+
|
|
68
|
+
# Event bindings
|
|
69
|
+
self.tree.bind("<<TreeviewSelect>>", self._on_selection_change)
|
|
70
|
+
self.tree.bind("<Button-3>", self._popup)
|
|
71
|
+
|
|
72
|
+
# Context menu
|
|
73
|
+
self.menu = tk.Menu(self, tearoff=0)
|
|
74
|
+
self.menu.add_command(label="Delete", command=self.delete_selected)
|
|
75
|
+
|
|
76
|
+
# Store full agent data for details popup
|
|
77
|
+
self._agents_data = {}
|
|
78
|
+
|
|
79
|
+
# Cache selection state to avoid redundant tree.selection() calls
|
|
80
|
+
self._cached_selection = None
|
|
81
|
+
self._selection_is_dirty = True
|
|
82
|
+
|
|
83
|
+
# Initialize button states without triggering immediate API calls
|
|
84
|
+
self._update_button_states()
|
|
85
|
+
|
|
86
|
+
def _get_cached_auth_state(self) -> bool:
|
|
87
|
+
"""Get authentication state with local caching to reduce API calls."""
|
|
88
|
+
now = time.time()
|
|
89
|
+
|
|
90
|
+
# Use cached result if still fresh
|
|
91
|
+
if (self._cached_auth_state is not None and
|
|
92
|
+
(now - self._last_auth_check) < self._auth_cache_duration):
|
|
93
|
+
return self._cached_auth_state
|
|
94
|
+
|
|
95
|
+
# Check authentication and cache result
|
|
96
|
+
self._cached_auth_state = self.api.is_authenticated
|
|
97
|
+
self._last_auth_check = now
|
|
98
|
+
return self._cached_auth_state
|
|
99
|
+
|
|
100
|
+
def _get_selection(self):
|
|
101
|
+
"""Get cached selection to avoid redundant tree.selection() calls."""
|
|
102
|
+
if self._selection_is_dirty or self._cached_selection is None:
|
|
103
|
+
self._cached_selection = self.tree.selection()
|
|
104
|
+
self._selection_is_dirty = False
|
|
105
|
+
return self._cached_selection
|
|
106
|
+
|
|
107
|
+
def _update_button_states(self):
|
|
108
|
+
"""Update button states based on current conditions - IMMEDIATE, no timers."""
|
|
109
|
+
# Refresh button - always enabled if authenticated (use cached state)
|
|
110
|
+
is_auth = self._get_cached_auth_state()
|
|
111
|
+
self.refresh_btn.set_enabled(
|
|
112
|
+
is_auth,
|
|
113
|
+
"Authentication required" if not is_auth else ""
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Get selection once and cache it
|
|
117
|
+
selection = self._get_selection()
|
|
118
|
+
has_selection = bool(selection)
|
|
119
|
+
single_selection = len(selection) == 1
|
|
120
|
+
|
|
121
|
+
# Delete button - enabled only if agents are selected
|
|
122
|
+
self.delete_btn.set_enabled(
|
|
123
|
+
has_selection,
|
|
124
|
+
"Select agents to delete" if not has_selection else ""
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Details button - enabled only if a single agent is selected
|
|
128
|
+
self.details_btn.set_enabled(
|
|
129
|
+
single_selection,
|
|
130
|
+
"Select a single agent to view details" if not single_selection else ""
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _on_selection_change(self, event=None):
|
|
134
|
+
"""Handle tree selection changes - IMMEDIATE update, no debouncing."""
|
|
135
|
+
# Mark selection cache as dirty
|
|
136
|
+
self._selection_is_dirty = True
|
|
137
|
+
# Immediate update - no timers or delays
|
|
138
|
+
self._update_button_states()
|
|
139
|
+
|
|
140
|
+
def _fill_tree_chunked(self, rows, start=0, chunk=200):
|
|
141
|
+
"""Fill tree in chunks to prevent UI freezing on large datasets."""
|
|
142
|
+
end = min(start + chunk, len(rows))
|
|
143
|
+
for agent in rows[start:end]:
|
|
144
|
+
iid = self.tree.insert("", "end", values=(
|
|
145
|
+
agent["id"],
|
|
146
|
+
agent["display_name"],
|
|
147
|
+
agent["authorization_id"],
|
|
148
|
+
agent["engine_id"]
|
|
149
|
+
))
|
|
150
|
+
# Store full agent data for details popup
|
|
151
|
+
self._agents_data[iid] = agent
|
|
152
|
+
|
|
153
|
+
if end < len(rows):
|
|
154
|
+
# Yield to UI thread, then continue with next chunk
|
|
155
|
+
self.after(0, self._fill_tree_chunked, rows, end, chunk)
|
|
156
|
+
else:
|
|
157
|
+
# All done - update final status and button states
|
|
158
|
+
count = len(rows)
|
|
159
|
+
self.status.set(f"Loaded {count} agent{'s' if count != 1 else ''}.")
|
|
160
|
+
self.log(f"β
Loaded {count} agent space agent{'s' if count != 1 else ''}. Data keys: {len(self._agents_data)}")
|
|
161
|
+
self._update_button_states()
|
|
162
|
+
|
|
163
|
+
def refresh(self):
|
|
164
|
+
"""Refresh the agent list from the API."""
|
|
165
|
+
if not self._get_cached_auth_state():
|
|
166
|
+
self.log("π Authentication required.")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# Show loading dialog
|
|
170
|
+
loading_dialog = LoadingDialog(self.winfo_toplevel(), "Loading agents...")
|
|
171
|
+
self.refresh_btn.set_enabled(False, "Loading...")
|
|
172
|
+
self.status.set("Loading agents...")
|
|
173
|
+
|
|
174
|
+
def callback(result):
|
|
175
|
+
# Close loading dialog
|
|
176
|
+
loading_dialog.close()
|
|
177
|
+
self.refresh_btn.set_enabled(True)
|
|
178
|
+
|
|
179
|
+
if isinstance(result, Exception):
|
|
180
|
+
self.status.set(f"Error: {result}")
|
|
181
|
+
self.log(f"β Error loading agents: {result}")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Bulk delete all existing rows (efficient)
|
|
185
|
+
self.tree.delete(*self.tree.get_children())
|
|
186
|
+
|
|
187
|
+
# Clear stored data and invalidate selection cache
|
|
188
|
+
self._agents_data.clear()
|
|
189
|
+
self._selection_is_dirty = True
|
|
190
|
+
|
|
191
|
+
# Fill tree in chunks to prevent UI freezing
|
|
192
|
+
if result:
|
|
193
|
+
self._fill_tree_chunked(result)
|
|
194
|
+
else:
|
|
195
|
+
# No agents
|
|
196
|
+
self.status.set("No agents found.")
|
|
197
|
+
self.log("βΉοΈ No agents found.")
|
|
198
|
+
self._update_button_states()
|
|
199
|
+
|
|
200
|
+
async_operation(self.api.list_agent_space_agents, callback=callback, ui_widget=self)
|
|
201
|
+
|
|
202
|
+
def delete_selected(self):
|
|
203
|
+
"""Delete selected agents."""
|
|
204
|
+
# Use cached selection to avoid redundant tree.selection() call
|
|
205
|
+
selection = self._get_selection()
|
|
206
|
+
if not selection:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
count = len(selection)
|
|
210
|
+
if not messagebox.askyesno("Confirm", f"Delete {count} agent{'s' if count != 1 else ''}?"):
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Get all selected agents
|
|
214
|
+
agents_to_delete = []
|
|
215
|
+
for item in selection:
|
|
216
|
+
values = self.tree.item(item, "values")
|
|
217
|
+
if len(values) >= 4:
|
|
218
|
+
agent_id = values[0]
|
|
219
|
+
display_name = values[1]
|
|
220
|
+
# Find full_name from the API data
|
|
221
|
+
full_name = f"projects/{self.api.project_id}/locations/global/collections/default_collection/engines/{self.api.engine_name}/assistants/default_assistant/agents/{agent_id}"
|
|
222
|
+
agents_to_delete.append((item, agent_id, display_name, full_name))
|
|
223
|
+
|
|
224
|
+
if not agents_to_delete:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
self.status.set(f"Deleting {count} agent{'s' if count != 1 else ''}...")
|
|
228
|
+
|
|
229
|
+
def delete_next(index=0):
|
|
230
|
+
if index >= len(agents_to_delete):
|
|
231
|
+
# All done
|
|
232
|
+
self.status.set(f"Deleted {count} agent{'s' if count != 1 else ''}.")
|
|
233
|
+
self.log(f"β
Deleted {count} agent{'s' if count != 1 else ''}.")
|
|
234
|
+
# Update button states after deletion
|
|
235
|
+
self._update_button_states()
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
item, agent_id, display_name, full_name = agents_to_delete[index]
|
|
239
|
+
|
|
240
|
+
def callback(result):
|
|
241
|
+
if isinstance(result, Exception):
|
|
242
|
+
self.log(f"β Failed to delete {display_name}: {result}")
|
|
243
|
+
else:
|
|
244
|
+
status, message = result
|
|
245
|
+
if status == "deleted":
|
|
246
|
+
self.tree.delete(item)
|
|
247
|
+
# Invalidate selection cache since tree changed
|
|
248
|
+
self._selection_is_dirty = True
|
|
249
|
+
self.log(f"β
Deleted agent: {display_name}")
|
|
250
|
+
else:
|
|
251
|
+
self.log(f"β οΈ {display_name}: {message}")
|
|
252
|
+
|
|
253
|
+
# Continue with next deletion
|
|
254
|
+
delete_next(index + 1)
|
|
255
|
+
|
|
256
|
+
async_operation(lambda: self.api.delete_agent_from_space(full_name), callback=callback, ui_widget=self)
|
|
257
|
+
|
|
258
|
+
delete_next()
|
|
259
|
+
|
|
260
|
+
def show_agent_details(self):
|
|
261
|
+
"""Show details for the selected agent."""
|
|
262
|
+
# Use cached selection to avoid redundant tree.selection() call
|
|
263
|
+
sel = self._get_selection()
|
|
264
|
+
if len(sel) != 1:
|
|
265
|
+
messagebox.showinfo("No selection", "Select a single agent to view details.")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
item_id = sel[0]
|
|
269
|
+
if item_id not in self._agents_data:
|
|
270
|
+
messagebox.showerror("Error", "Agent data not found.")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
agent_data = self._agents_data[item_id]
|
|
274
|
+
try:
|
|
275
|
+
AgentDetailsDialog(self, agent_data)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"Error opening agent details dialog: {e}")
|
|
278
|
+
messagebox.showerror("Error", f"Failed to open details dialog: {e}")
|
|
279
|
+
|
|
280
|
+
def _popup(self, event):
|
|
281
|
+
"""Show context menu."""
|
|
282
|
+
row = self.tree.identify_row(event.y)
|
|
283
|
+
if not row:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# Always select the right-clicked row for clarity
|
|
287
|
+
# This ensures context menu operates on the visible selection
|
|
288
|
+
self.tree.selection_set(row)
|
|
289
|
+
self._selection_is_dirty = True
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
self.menu.tk_popup(event.x_root, event.y_root)
|
|
293
|
+
finally:
|
|
294
|
+
self.menu.grab_release()
|
|
295
|
+
|
|
296
|
+
def update_api(self, api: ApiClient):
|
|
297
|
+
"""Update the API client reference."""
|
|
298
|
+
self.api = api
|
|
299
|
+
# Clear cached auth state when API changes
|
|
300
|
+
self._cached_auth_state = None
|
|
301
|
+
self._last_auth_check = 0
|
|
302
|
+
# Update button states immediately
|
|
303
|
+
self._update_button_states()
|
|
304
|
+
|
|
305
|
+
# Add validation for required fields
|
|
306
|
+
def validate_env(self):
|
|
307
|
+
required_fields = ["project_id", "project_number", "location", "agent_space", "engine"]
|
|
308
|
+
missing_fields = [field for field in required_fields if not getattr(self.api, field, None)]
|
|
309
|
+
if missing_fields:
|
|
310
|
+
messagebox.showerror("Validation Error", f"Missing required fields: {', '.join(missing_fields)}")
|
|
311
|
+
return False
|
|
312
|
+
return True
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Authorization management view."""
|
|
2
|
+
import time
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from tkinter import ttk, messagebox
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from ..core.api_client import ApiClient
|
|
8
|
+
from .ui_components import async_operation, StatusButton, LoadingDialog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthorizationView(ttk.Frame):
|
|
12
|
+
"""UI for managing OAuth authorizations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, master, api: ApiClient, log: Callable[[str], None]):
|
|
15
|
+
super().__init__(master)
|
|
16
|
+
self.api = api
|
|
17
|
+
self.log = log
|
|
18
|
+
self._auth_auto_loaded = False # Track if authorizations have been auto-loaded
|
|
19
|
+
|
|
20
|
+
# Cache authentication state to avoid repeated API calls
|
|
21
|
+
self._cached_auth_state = None
|
|
22
|
+
self._last_auth_check = 0
|
|
23
|
+
self._auth_cache_duration = 30 # 30 seconds
|
|
24
|
+
|
|
25
|
+
self._setup_ui()
|
|
26
|
+
|
|
27
|
+
def _setup_ui(self):
|
|
28
|
+
# Control buttons
|
|
29
|
+
btns = ttk.Frame(self)
|
|
30
|
+
btns.pack(fill="x", pady=(6, 4))
|
|
31
|
+
|
|
32
|
+
self.refresh_btn = StatusButton(btns, text="Refresh Authorizations", command=self.refresh)
|
|
33
|
+
self.refresh_btn.pack(side="left", padx=4)
|
|
34
|
+
|
|
35
|
+
self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
|
|
36
|
+
self.delete_btn.pack(side="left", padx=8)
|
|
37
|
+
|
|
38
|
+
self.status = tk.StringVar(value="Ready.")
|
|
39
|
+
ttk.Label(btns, textvariable=self.status).pack(side="right")
|
|
40
|
+
|
|
41
|
+
# Info label
|
|
42
|
+
info_frame = ttk.Frame(self)
|
|
43
|
+
info_frame.pack(fill="x", padx=4, pady=4)
|
|
44
|
+
ttk.Label(info_frame, text="π‘ Authorizations enable agents to access Google Drive, Sheets, etc.",
|
|
45
|
+
foreground="gray").pack(anchor="w")
|
|
46
|
+
|
|
47
|
+
# Authorization list
|
|
48
|
+
wrap = ttk.Frame(self)
|
|
49
|
+
wrap.pack(fill="both", expand=True)
|
|
50
|
+
cols = ("id", "name")
|
|
51
|
+
self.tree = ttk.Treeview(wrap, columns=cols, show="headings", selectmode="extended")
|
|
52
|
+
|
|
53
|
+
for c, t, w in [
|
|
54
|
+
("id", "Authorization ID", 300),
|
|
55
|
+
("name", "Full Resource Name", 600),
|
|
56
|
+
]:
|
|
57
|
+
self.tree.heading(c, text=t)
|
|
58
|
+
self.tree.column(c, width=w, anchor="w")
|
|
59
|
+
|
|
60
|
+
self.tree.pack(side="left", fill="both", expand=True)
|
|
61
|
+
vsb = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
|
|
62
|
+
self.tree.configure(yscroll=vsb.set)
|
|
63
|
+
vsb.pack(side="right", fill="y")
|
|
64
|
+
|
|
65
|
+
# Event bindings
|
|
66
|
+
self.tree.bind("<<TreeviewSelect>>", self._on_selection_change)
|
|
67
|
+
self.tree.bind("<Button-3>", self._popup)
|
|
68
|
+
|
|
69
|
+
# Context menu
|
|
70
|
+
self.menu = tk.Menu(self, tearoff=0)
|
|
71
|
+
self.menu.add_command(label="Delete", command=self.delete_selected)
|
|
72
|
+
|
|
73
|
+
# Debouncing for button updates
|
|
74
|
+
self._update_timer = None
|
|
75
|
+
|
|
76
|
+
# Store full authorization data
|
|
77
|
+
self._authorizations_data = {}
|
|
78
|
+
|
|
79
|
+
# Cache selection state to avoid redundant tree.selection() calls
|
|
80
|
+
self._cached_selection = None
|
|
81
|
+
self._selection_is_dirty = True
|
|
82
|
+
|
|
83
|
+
# Initialize button states without immediate API calls
|
|
84
|
+
self._update_button_states()
|
|
85
|
+
|
|
86
|
+
def _get_cached_auth_state(self) -> bool:
|
|
87
|
+
"""Get authentication state with local caching to reduce API calls."""
|
|
88
|
+
now = time.time()
|
|
89
|
+
|
|
90
|
+
# Use cached result if still fresh
|
|
91
|
+
if (self._cached_auth_state is not None and
|
|
92
|
+
(now - self._last_auth_check) < self._auth_cache_duration):
|
|
93
|
+
return self._cached_auth_state
|
|
94
|
+
|
|
95
|
+
# Check authentication and cache result
|
|
96
|
+
self._cached_auth_state = self.api.is_authenticated
|
|
97
|
+
self._last_auth_check = now
|
|
98
|
+
return self._cached_auth_state
|
|
99
|
+
|
|
100
|
+
def _get_selection(self):
|
|
101
|
+
"""Get cached selection to avoid redundant tree.selection() calls."""
|
|
102
|
+
if self._selection_is_dirty or self._cached_selection is None:
|
|
103
|
+
self._cached_selection = self.tree.selection()
|
|
104
|
+
self._selection_is_dirty = False
|
|
105
|
+
return self._cached_selection
|
|
106
|
+
|
|
107
|
+
def _update_button_states(self):
|
|
108
|
+
"""Update button states based on current conditions."""
|
|
109
|
+
# Use cached authentication state to reduce API calls
|
|
110
|
+
is_auth = self._get_cached_auth_state()
|
|
111
|
+
self.refresh_btn.set_enabled(
|
|
112
|
+
is_auth,
|
|
113
|
+
"Authentication required" if not is_auth else ""
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Delete button - enabled only if authorizations are selected
|
|
117
|
+
has_selection = bool(self._get_selection())
|
|
118
|
+
self.delete_btn.set_enabled(
|
|
119
|
+
has_selection,
|
|
120
|
+
"Select authorizations to delete" if not has_selection else ""
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _on_selection_change(self, event=None):
|
|
124
|
+
"""Handle selection changes - IMMEDIATE update, no debouncing."""
|
|
125
|
+
# Mark selection cache as dirty
|
|
126
|
+
self._selection_is_dirty = True
|
|
127
|
+
# Immediate update - no timers or delays
|
|
128
|
+
self._update_button_states()
|
|
129
|
+
|
|
130
|
+
def refresh(self):
|
|
131
|
+
"""Refresh the list of authorizations."""
|
|
132
|
+
# Update button states immediately on click
|
|
133
|
+
self._update_button_states()
|
|
134
|
+
|
|
135
|
+
# Check authentication once at the start using cached state
|
|
136
|
+
if not self._get_cached_auth_state():
|
|
137
|
+
self.log("β Authentication required")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Show loading dialog
|
|
141
|
+
loading_dialog = LoadingDialog(self.winfo_toplevel(), "Loading authorizations...")
|
|
142
|
+
self.refresh_btn.set_enabled(False, "Loading...")
|
|
143
|
+
|
|
144
|
+
def callback(items):
|
|
145
|
+
# Close loading dialog
|
|
146
|
+
loading_dialog.close()
|
|
147
|
+
self.refresh_btn.set_enabled(True)
|
|
148
|
+
|
|
149
|
+
# Bulk delete all rows at once instead of one-by-one
|
|
150
|
+
self.tree.delete(*self.tree.get_children())
|
|
151
|
+
|
|
152
|
+
# Clear stored data
|
|
153
|
+
self._authorizations_data.clear()
|
|
154
|
+
|
|
155
|
+
if isinstance(items, Exception):
|
|
156
|
+
self.log(f"β List error: {items}")
|
|
157
|
+
self.status.set("Error")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Use chunked filling for large datasets to avoid UI blocking
|
|
161
|
+
self._fill_tree_chunked(items)
|
|
162
|
+
|
|
163
|
+
async_operation(self.api.list_authorizations, callback=callback, ui_widget=self)
|
|
164
|
+
|
|
165
|
+
def _fill_tree_chunked(self, rows, start=0, chunk=200):
|
|
166
|
+
"""Fill tree in chunks to avoid UI blocking with large datasets."""
|
|
167
|
+
end = min(start + chunk, len(rows))
|
|
168
|
+
for it in rows[start:end]:
|
|
169
|
+
# Insert into tree
|
|
170
|
+
item_id = self.tree.insert("", "end", values=(it["id"], it["name"]))
|
|
171
|
+
# Store full data for future use
|
|
172
|
+
self._authorizations_data[item_id] = it
|
|
173
|
+
|
|
174
|
+
if end < len(rows):
|
|
175
|
+
# Yield to UI thread, then continue with next chunk
|
|
176
|
+
self.after(0, self._fill_tree_chunked, rows, end, chunk)
|
|
177
|
+
else:
|
|
178
|
+
# All done - update status and button states
|
|
179
|
+
self.status.set(f"{len(rows)} authorization(s)")
|
|
180
|
+
self._update_button_states()
|
|
181
|
+
|
|
182
|
+
def delete_selected(self):
|
|
183
|
+
"""Delete selected authorizations."""
|
|
184
|
+
# Use cached selection to avoid redundant tree.selection() call
|
|
185
|
+
sel = self._get_selection()
|
|
186
|
+
if not sel:
|
|
187
|
+
messagebox.showinfo("No selection", "Select one or more authorizations to delete.")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Get authorization IDs from stored data
|
|
191
|
+
auth_ids = []
|
|
192
|
+
for item_id in sel:
|
|
193
|
+
if item_id in self._authorizations_data:
|
|
194
|
+
auth_ids.append(self._authorizations_data[item_id]["id"])
|
|
195
|
+
|
|
196
|
+
if not auth_ids:
|
|
197
|
+
messagebox.showerror("Error", "Could not find authorization IDs for selected items.")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Confirm deletion
|
|
201
|
+
count = len(auth_ids)
|
|
202
|
+
msg = f"Delete {count} authorization{'s' if count > 1 else ''}?\n\n"
|
|
203
|
+
msg += "\n".join(f"β’ {auth_id}" for auth_id in auth_ids[:5])
|
|
204
|
+
if count > 5:
|
|
205
|
+
msg += f"\n... and {count - 5} more"
|
|
206
|
+
|
|
207
|
+
if not messagebox.askyesno("Confirm deletion", msg):
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
self.log(f"ποΈ Deleting {count} authorization{'s' if count > 1 else ''}...")
|
|
211
|
+
self.delete_btn.set_enabled(False, "Deleting...")
|
|
212
|
+
|
|
213
|
+
def callback(results):
|
|
214
|
+
self.delete_btn.set_enabled(True)
|
|
215
|
+
|
|
216
|
+
if isinstance(results, Exception):
|
|
217
|
+
self.log(f"β Delete error: {results}")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Batch log messages to reduce UI spam
|
|
221
|
+
ok = [k for k, (s, _) in results.items() if s == "deleted"]
|
|
222
|
+
bad = {k: v for k, v in results.items() if v[0] != "deleted"}
|
|
223
|
+
|
|
224
|
+
if ok:
|
|
225
|
+
ok_display = ", ".join(ok[:10]) + ("β¦" if len(ok) > 10 else "")
|
|
226
|
+
self.log(f"β
Deleted {len(ok)} authorization(s): {ok_display}")
|
|
227
|
+
|
|
228
|
+
# Show first 10 failures with details
|
|
229
|
+
for k, (s, msg) in list(bad.items())[:10]:
|
|
230
|
+
self.log(f"β {k}: {msg}")
|
|
231
|
+
if len(bad) > 10:
|
|
232
|
+
self.log(f"β¦and {len(bad)-10} more failures")
|
|
233
|
+
|
|
234
|
+
self._update_button_states()
|
|
235
|
+
# Auto-refresh to show updated list
|
|
236
|
+
self.refresh()
|
|
237
|
+
|
|
238
|
+
# Delete authorizations sequentially (to avoid API rate limits)
|
|
239
|
+
def delete_multiple():
|
|
240
|
+
results = {}
|
|
241
|
+
for auth_id in auth_ids:
|
|
242
|
+
try:
|
|
243
|
+
results[auth_id] = self.api.delete_authorization(auth_id)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
results[auth_id] = ("failed", str(e))
|
|
246
|
+
return results
|
|
247
|
+
|
|
248
|
+
async_operation(delete_multiple, callback=callback, ui_widget=self)
|
|
249
|
+
|
|
250
|
+
def _popup(self, event):
|
|
251
|
+
"""Show context menu."""
|
|
252
|
+
try:
|
|
253
|
+
self.menu.tk_popup(event.x_root, event.y_root)
|
|
254
|
+
finally:
|
|
255
|
+
self.menu.grab_release()
|
|
256
|
+
|
|
257
|
+
def update_api(self, api: ApiClient):
|
|
258
|
+
"""Update the API client reference."""
|
|
259
|
+
self.api = api
|
|
260
|
+
# Clear cached auth state when API changes
|
|
261
|
+
self._cached_auth_state = None
|
|
262
|
+
self._last_auth_check = 0
|
|
263
|
+
# Reset auto-load flag
|
|
264
|
+
self._auth_auto_loaded = False
|
|
265
|
+
# Update button states immediately
|
|
266
|
+
self._update_button_states()
|
|
267
|
+
|
|
268
|
+
# Auto-load authorizations if credentials are available and not loaded yet
|
|
269
|
+
if self._get_cached_auth_state():
|
|
270
|
+
self._auth_auto_loaded = True
|
|
271
|
+
self.log("β
Auto-loading authorizations...")
|
|
272
|
+
# Use a small delay to ensure UI is ready
|
|
273
|
+
self.after(50, self.refresh)
|
|
274
|
+
|
|
275
|
+
def on_tab_selected(self):
|
|
276
|
+
"""Called when this tab is selected - trigger auto-loading if needed."""
|
|
277
|
+
if not self._auth_auto_loaded and self._get_cached_auth_state():
|
|
278
|
+
self._auth_auto_loaded = True
|
|
279
|
+
self.log("β
Auto-loading authorizations...")
|
|
280
|
+
self.refresh()
|