claude's gae
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
@echo off
|
||||||
|
echo Building Azure Key Vault Secret Manager...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Clean previous builds
|
||||||
|
if exist "dist" rmdir /s /q "dist"
|
||||||
|
if exist "build" rmdir /s /q "build"
|
||||||
|
|
||||||
|
REM Build executable
|
||||||
|
pyinstaller build.spec --clean --noconfirm
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Build complete!
|
||||||
|
echo Executable location: dist\AzureKeyVaultSecretManager.exe
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Building Azure Key Vault Secret Manager..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean previous builds
|
||||||
|
rm -rf dist build
|
||||||
|
|
||||||
|
# Build executable
|
||||||
|
pyinstaller build.spec --clean --noconfirm
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Build complete!"
|
||||||
|
echo "Executable location: dist/AzureKeyVaultSecretManager.exe"
|
||||||
|
echo ""
|
||||||
|
read -p "Press enter to continue..."
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
PyInstaller runtime hook for CustomTkinter.
|
||||||
|
Ensures theme assets are found in bundled executable.
|
||||||
|
"""
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||||
|
|
||||||
|
datas = collect_data_files('customtkinter')
|
||||||
|
hiddenimports = collect_submodules('customtkinter')
|
||||||
@@ -66,6 +66,12 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
self.item_buttons: List[ctk.CTkButton] = []
|
self.item_buttons: List[ctk.CTkButton] = []
|
||||||
self.item_tooltips: List[ToolTip] = []
|
self.item_tooltips: List[ToolTip] = []
|
||||||
|
|
||||||
|
# Search state
|
||||||
|
self.all_items: List[Dict] = [] # Store complete unfiltered list
|
||||||
|
self.filtered_items: List[Dict] = [] # Currently filtered items
|
||||||
|
self.search_entry: Optional[ctk.CTkEntry] = None
|
||||||
|
self.search_query: str = ""
|
||||||
|
|
||||||
# Popup window
|
# Popup window
|
||||||
self.popup_window: Optional[tk.Toplevel] = None
|
self.popup_window: Optional[tk.Toplevel] = None
|
||||||
self.popup_frame: Optional[ctk.CTkScrollableFrame] = None
|
self.popup_frame: Optional[ctk.CTkScrollableFrame] = None
|
||||||
@@ -133,7 +139,10 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
Args:
|
Args:
|
||||||
items: List of item dictionaries
|
items: List of item dictionaries
|
||||||
"""
|
"""
|
||||||
self.items = items
|
# Store complete list and initialize filtered list
|
||||||
|
self.all_items = items
|
||||||
|
self.filtered_items = items.copy()
|
||||||
|
self.items = self.filtered_items
|
||||||
self.current_index = -1
|
self.current_index = -1
|
||||||
|
|
||||||
# Update count label
|
# Update count label
|
||||||
@@ -202,6 +211,7 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
self.popup_window = tk.Toplevel(self)
|
self.popup_window = tk.Toplevel(self)
|
||||||
self.popup_window.wm_overrideredirect(True) # Remove window decorations
|
self.popup_window.wm_overrideredirect(True) # Remove window decorations
|
||||||
self.popup_window.wm_attributes("-topmost", True) # Always on top
|
self.popup_window.wm_attributes("-topmost", True) # Always on top
|
||||||
|
self.popup_window.wm_resizable(False, False) # Prevent resizing
|
||||||
|
|
||||||
# Set background color to prevent white flash
|
# Set background color to prevent white flash
|
||||||
appearance_mode = ctk.get_appearance_mode()
|
appearance_mode = ctk.get_appearance_mode()
|
||||||
@@ -213,6 +223,20 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
|
|
||||||
self.popup_window.withdraw() # Hide initially to prevent flash in top-left corner
|
self.popup_window.withdraw() # Hide initially to prevent flash in top-left corner
|
||||||
|
|
||||||
|
# Create search entry at top of popup
|
||||||
|
self.search_entry = ctk.CTkEntry(
|
||||||
|
self.popup_window,
|
||||||
|
placeholder_text="Search...",
|
||||||
|
height=32,
|
||||||
|
font=ctk.CTkFont(size=13)
|
||||||
|
)
|
||||||
|
self.search_entry.pack(fill="x", padx=5, pady=(5, 0))
|
||||||
|
self.search_entry.focus_set()
|
||||||
|
|
||||||
|
# Bind search events
|
||||||
|
self.search_entry.bind("<KeyRelease>", self._on_search_changed)
|
||||||
|
self.search_entry.bind("<Escape>", lambda e: self._close_dropdown())
|
||||||
|
|
||||||
# Calculate dynamic height based on number of items
|
# Calculate dynamic height based on number of items
|
||||||
# Show first 10 items (or all if less than 10)
|
# Show first 10 items (or all if less than 10)
|
||||||
# Each item: 40px button + 2px borders (1px top + 1px bottom) + 4px padding (2px top + 2px bottom) = 46px per item
|
# Each item: 40px button + 2px borders (1px top + 1px bottom) + 4px padding (2px top + 2px bottom) = 46px per item
|
||||||
@@ -220,25 +244,27 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
max_visible_items = 10
|
max_visible_items = 10
|
||||||
items_to_show = min(len(self.items), max_visible_items)
|
items_to_show = min(len(self.items), max_visible_items)
|
||||||
|
|
||||||
# Height calculation: (items × 46px) + extra padding for frame borders
|
# Height calculation: (items × 46px) + extra padding for frame borders + search box height
|
||||||
# Add 20px padding: 10px top + 10px bottom (5px pack padding + 5px extra for scrollbar/borders)
|
# Add 62px total: 42px for search box (32px height + 5px top padding + 5px spacing) + 20px for frame borders
|
||||||
calculated_height = (items_to_show * item_height) + 20
|
calculated_height = (items_to_show * item_height) + 62
|
||||||
|
|
||||||
self._popup_height = calculated_height
|
self._popup_height = calculated_height
|
||||||
|
|
||||||
# Set window size explicitly to ensure proper height
|
# Set window size explicitly and fix dimensions
|
||||||
self.popup_window.wm_geometry(f"{self.button_width}x{self._popup_height}")
|
self.popup_window.wm_geometry(f"{self.button_width}x{self._popup_height}")
|
||||||
|
self.popup_window.wm_minsize(self.button_width, self._popup_height)
|
||||||
|
self.popup_window.wm_maxsize(self.button_width, self._popup_height)
|
||||||
|
|
||||||
# Create scrollable frame for items with fixed width
|
# Create scrollable frame for items with fixed width
|
||||||
# Frame height = window height - padding (10px pack padding total)
|
# Frame height = window height - padding - search box height (10px pack padding total + 42px search)
|
||||||
frame_height = self._popup_height - 10
|
frame_height = self._popup_height - 52
|
||||||
|
|
||||||
self.popup_frame = ctk.CTkScrollableFrame(
|
self.popup_frame = ctk.CTkScrollableFrame(
|
||||||
self.popup_window,
|
self.popup_window,
|
||||||
width=self.button_width - 20,
|
width=self.button_width - 20,
|
||||||
height=frame_height
|
height=frame_height
|
||||||
)
|
)
|
||||||
self.popup_frame.pack(fill="both", expand=False, padx=5, pady=5)
|
self.popup_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
# Configure scroll speed to match main window (40px per scroll unit = 2x faster)
|
# Configure scroll speed to match main window (40px per scroll unit = 2x faster)
|
||||||
self.popup_frame._parent_canvas.configure(yscrollincrement=40)
|
self.popup_frame._parent_canvas.configure(yscrollincrement=40)
|
||||||
@@ -275,7 +301,8 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
border_width=1,
|
border_width=1,
|
||||||
border_color="gray50",
|
border_color="gray50",
|
||||||
hover_color=("gray70", "gray30"),
|
hover_color=("gray70", "gray30"),
|
||||||
anchor="w"
|
anchor="w",
|
||||||
|
text_color=("gray10", "gray90")
|
||||||
)
|
)
|
||||||
btn.grid(row=idx, column=0, padx=5, pady=pady_val, sticky="ew")
|
btn.grid(row=idx, column=0, padx=5, pady=pady_val, sticky="ew")
|
||||||
self.popup_frame.grid_columnconfigure(0, weight=1)
|
self.popup_frame.grid_columnconfigure(0, weight=1)
|
||||||
@@ -312,19 +339,30 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
# Position popup
|
# Position popup
|
||||||
self._position_popup()
|
self._position_popup()
|
||||||
|
|
||||||
# Bind keyboard events (use correct Tkinter key names)
|
# Bind keyboard events to search entry for navigation
|
||||||
|
self.search_entry.bind("<Down>", lambda e: self._focus_first_item())
|
||||||
|
self.search_entry.bind("<Up>", lambda e: self._navigate_end())
|
||||||
|
self.search_entry.bind("<Next>", self._navigate_page_down) # PageDown in Tkinter
|
||||||
|
self.search_entry.bind("<Prior>", self._navigate_page_up) # PageUp in Tkinter
|
||||||
|
self.search_entry.bind("<Home>", self._navigate_home)
|
||||||
|
self.search_entry.bind("<End>", self._navigate_end)
|
||||||
|
self.search_entry.bind("<Return>", self._confirm_selection)
|
||||||
|
|
||||||
|
# Bind keyboard events to popup window as well (for when focus moves)
|
||||||
self.popup_window.bind("<Escape>", lambda e: self._close_dropdown())
|
self.popup_window.bind("<Escape>", lambda e: self._close_dropdown())
|
||||||
self.popup_window.bind("<Down>", self._navigate_down)
|
self.popup_window.bind("<Down>", self._navigate_down)
|
||||||
self.popup_window.bind("<Up>", self._navigate_up)
|
self.popup_window.bind("<Up>", self._navigate_up)
|
||||||
self.popup_window.bind("<Next>", self._navigate_page_down) # PageDown in Tkinter
|
self.popup_window.bind("<Next>", self._navigate_page_down)
|
||||||
self.popup_window.bind("<Prior>", self._navigate_page_up) # PageUp in Tkinter
|
self.popup_window.bind("<Prior>", self._navigate_page_up)
|
||||||
self.popup_window.bind("<Home>", self._navigate_home)
|
self.popup_window.bind("<Home>", self._navigate_home)
|
||||||
self.popup_window.bind("<End>", self._navigate_end)
|
self.popup_window.bind("<End>", self._navigate_end)
|
||||||
self.popup_window.bind("<Return>", self._confirm_selection)
|
self.popup_window.bind("<Return>", self._confirm_selection)
|
||||||
# Removed FocusOut binding - it causes race conditions with button clicks
|
|
||||||
|
|
||||||
# Set focus to popup
|
# Bind any printable character to redirect focus to search entry
|
||||||
self.popup_window.focus_set()
|
self.popup_window.bind("<Key>", self._redirect_to_search)
|
||||||
|
|
||||||
|
# Keep focus on search entry (don't override with popup_window.focus_set())
|
||||||
|
# Focus was already set on search_entry at line 233
|
||||||
|
|
||||||
# Bind click outside to close
|
# Bind click outside to close
|
||||||
self.popup_window.bind("<Button-1>", self._check_click_outside, add="+")
|
self.popup_window.bind("<Button-1>", self._check_click_outside, add="+")
|
||||||
@@ -335,6 +373,12 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
# Set flag to prevent race conditions
|
# Set flag to prevent race conditions
|
||||||
self._closing = True
|
self._closing = True
|
||||||
|
|
||||||
|
# Clean up search state
|
||||||
|
self.search_entry = None
|
||||||
|
self.search_query = ""
|
||||||
|
self.filtered_items = self.all_items.copy()
|
||||||
|
self.items = self.filtered_items
|
||||||
|
|
||||||
# Clean up tooltips
|
# Clean up tooltips
|
||||||
for tooltip in self.item_tooltips:
|
for tooltip in self.item_tooltips:
|
||||||
tooltip.destroy()
|
tooltip.destroy()
|
||||||
@@ -560,6 +604,129 @@ class UnifiedDropdown(ctk.CTkFrame):
|
|||||||
# Note: CTkScrollableFrame uses internal canvas
|
# Note: CTkScrollableFrame uses internal canvas
|
||||||
btn.update_idletasks()
|
btn.update_idletasks()
|
||||||
|
|
||||||
|
def _on_search_changed(self, event=None):
|
||||||
|
"""Handle search query change and filter items."""
|
||||||
|
query = self.search_entry.get().strip().lower()
|
||||||
|
self.search_query = query
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
# Show all items
|
||||||
|
self.filtered_items = self.all_items.copy()
|
||||||
|
else:
|
||||||
|
# Filter items based on display text (case-insensitive)
|
||||||
|
self.filtered_items = [
|
||||||
|
item for item in self.all_items
|
||||||
|
if query in self._get_display_text(item).lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rebuild popup with filtered items
|
||||||
|
self._rebuild_popup_items()
|
||||||
|
|
||||||
|
def _rebuild_popup_items(self):
|
||||||
|
"""Rebuild popup item list with filtered items."""
|
||||||
|
if not self.popup_frame:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear existing buttons and tooltips
|
||||||
|
for btn in self.item_buttons:
|
||||||
|
btn.destroy()
|
||||||
|
self.item_buttons.clear()
|
||||||
|
|
||||||
|
for tooltip in self.item_tooltips:
|
||||||
|
tooltip.destroy()
|
||||||
|
self.item_tooltips.clear()
|
||||||
|
|
||||||
|
# Update items reference
|
||||||
|
self.items = self.filtered_items
|
||||||
|
|
||||||
|
# Show "no results" if empty
|
||||||
|
if not self.filtered_items:
|
||||||
|
no_results_label = ctk.CTkLabel(
|
||||||
|
self.popup_frame,
|
||||||
|
text="No matching items found",
|
||||||
|
font=ctk.CTkFont(size=14),
|
||||||
|
text_color="gray"
|
||||||
|
)
|
||||||
|
no_results_label.grid(row=0, column=0, padx=10, pady=20)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Recreate buttons
|
||||||
|
for idx, item in enumerate(self.filtered_items):
|
||||||
|
display_text = self._get_display_text(item)
|
||||||
|
max_chars = 60
|
||||||
|
truncated = display_text if len(display_text) <= max_chars else display_text[:max_chars] + "..."
|
||||||
|
|
||||||
|
# Add extra padding for first and last items
|
||||||
|
if idx == 0:
|
||||||
|
pady_val = (3, 2)
|
||||||
|
elif idx == len(self.filtered_items) - 1:
|
||||||
|
pady_val = (2, 3)
|
||||||
|
else:
|
||||||
|
pady_val = 2
|
||||||
|
|
||||||
|
btn = ctk.CTkButton(
|
||||||
|
self.popup_frame,
|
||||||
|
text=truncated,
|
||||||
|
command=lambda i=idx: self._select_item(i, close_popup=True),
|
||||||
|
font=ctk.CTkFont(size=14),
|
||||||
|
height=40,
|
||||||
|
fg_color="transparent",
|
||||||
|
border_width=1,
|
||||||
|
border_color="gray50",
|
||||||
|
hover_color=("gray70", "gray30"),
|
||||||
|
anchor="w",
|
||||||
|
text_color=("gray10", "gray90")
|
||||||
|
)
|
||||||
|
btn.grid(row=idx, column=0, padx=5, pady=pady_val, sticky="ew")
|
||||||
|
self.popup_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Add tooltip if truncated
|
||||||
|
if len(display_text) > max_chars:
|
||||||
|
tooltip = ToolTip(btn, display_text, delay=500)
|
||||||
|
self.item_tooltips.append(tooltip)
|
||||||
|
|
||||||
|
# Bind mouse wheel
|
||||||
|
def popup_scroll(event):
|
||||||
|
if event.delta > 0:
|
||||||
|
self.popup_frame._parent_canvas.yview_scroll(-1, "units")
|
||||||
|
else:
|
||||||
|
self.popup_frame._parent_canvas.yview_scroll(1, "units")
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
btn.bind("<MouseWheel>", popup_scroll, add="+")
|
||||||
|
self.item_buttons.append(btn)
|
||||||
|
|
||||||
|
# Reset current index and highlight first item
|
||||||
|
self.current_index = 0
|
||||||
|
self._highlight_item(0)
|
||||||
|
|
||||||
|
def _focus_first_item(self):
|
||||||
|
"""Move focus from search to first item."""
|
||||||
|
if self.item_buttons:
|
||||||
|
self.current_index = 0
|
||||||
|
self._highlight_item(0)
|
||||||
|
self.popup_window.focus_set()
|
||||||
|
|
||||||
|
def _redirect_to_search(self, event):
|
||||||
|
"""Redirect keyboard input to search entry."""
|
||||||
|
# Ignore special keys that are already handled
|
||||||
|
if event.keysym in ['Escape', 'Down', 'Up', 'Next', 'Prior', 'Home', 'End', 'Return',
|
||||||
|
'Left', 'Right', 'Tab', 'Shift_L', 'Shift_R', 'Control_L',
|
||||||
|
'Control_R', 'Alt_L', 'Alt_R']:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Focus search entry if not already focused
|
||||||
|
if self.search_entry and str(self.focus_get()) != str(self.search_entry):
|
||||||
|
self.search_entry.focus_set()
|
||||||
|
# Insert the character that was typed
|
||||||
|
if len(event.char) == 1 and event.char.isprintable():
|
||||||
|
# Get current cursor position
|
||||||
|
current_pos = self.search_entry.index(tk.INSERT)
|
||||||
|
# Insert character at cursor position
|
||||||
|
self.search_entry.insert(current_pos, event.char)
|
||||||
|
# Trigger search update
|
||||||
|
self._on_search_changed()
|
||||||
|
|
||||||
def get_selected(self) -> Optional[Dict]:
|
def get_selected(self) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
Get the currently selected item.
|
Get the currently selected item.
|
||||||
|
|||||||
+15
-1
@@ -38,7 +38,21 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
# Configure window
|
# Configure window
|
||||||
self.title("Azure Key Vault Secret Manager")
|
self.title("Azure Key Vault Secret Manager")
|
||||||
self.geometry("950x850")
|
|
||||||
|
# Center window on screen
|
||||||
|
window_width = 950
|
||||||
|
window_height = 850
|
||||||
|
|
||||||
|
# Get screen dimensions
|
||||||
|
screen_width = self.winfo_screenwidth()
|
||||||
|
screen_height = self.winfo_screenheight()
|
||||||
|
|
||||||
|
# Calculate center position
|
||||||
|
center_x = int((screen_width - window_width) / 2)
|
||||||
|
center_y = int((screen_height - window_height) / 2)
|
||||||
|
|
||||||
|
# Set geometry with center position
|
||||||
|
self.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
|
||||||
|
|
||||||
# Set application icon (if available)
|
# Set application icon (if available)
|
||||||
self._set_icon()
|
self._set_icon()
|
||||||
|
|||||||
+22
-133
@@ -6,6 +6,7 @@ UI component for secret generation form.
|
|||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from typing import List, Dict, Callable, Optional
|
from typing import List, Dict, Callable, Optional
|
||||||
|
from ui.components.unified_dropdown import UnifiedDropdown
|
||||||
|
|
||||||
|
|
||||||
class SecretGenerationFrame(ctk.CTkFrame):
|
class SecretGenerationFrame(ctk.CTkFrame):
|
||||||
@@ -51,31 +52,18 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
)
|
)
|
||||||
self.description_entry.grid(row=2, column=0, columnspan=2, padx=20, pady=(0, 15), sticky="ew")
|
self.description_entry.grid(row=2, column=0, columnspan=2, padx=20, pady=(0, 15), sticky="ew")
|
||||||
|
|
||||||
# Key Vault Label
|
# Key Vault Dropdown (using UnifiedDropdown)
|
||||||
self.vault_label = ctk.CTkLabel(
|
self.vault_dropdown = UnifiedDropdown(
|
||||||
self,
|
self,
|
||||||
text="Select Key Vault:",
|
title="Select Key Vault:",
|
||||||
font=ctk.CTkFont(size=14)
|
on_selection_changed=self._on_vault_selected,
|
||||||
|
show_count=False,
|
||||||
|
display_format=lambda v: f"{v['name']} (RG: {v['resource_group']})",
|
||||||
|
max_dropdown_height=300,
|
||||||
|
button_width=600,
|
||||||
|
button_height=40
|
||||||
)
|
)
|
||||||
self.vault_label.grid(row=3, column=0, columnspan=2, padx=20, pady=(0, 5), sticky="w")
|
self.vault_dropdown.grid(row=3, column=0, columnspan=2, padx=0, pady=(0, 15), sticky="ew")
|
||||||
|
|
||||||
# Key Vault Dropdown Button (simplified inline version)
|
|
||||||
self.vault_dropdown_button = ctk.CTkButton(
|
|
||||||
self,
|
|
||||||
text="Please select a subscription first",
|
|
||||||
command=self._open_vault_dropdown,
|
|
||||||
width=600,
|
|
||||||
height=40,
|
|
||||||
font=ctk.CTkFont(size=14),
|
|
||||||
anchor="w",
|
|
||||||
state="disabled"
|
|
||||||
)
|
|
||||||
self.vault_dropdown_button.grid(row=4, column=0, columnspan=2, padx=20, pady=(0, 15), sticky="ew")
|
|
||||||
|
|
||||||
# Store vaults and selection
|
|
||||||
self.vaults = []
|
|
||||||
self.selected_vault = None
|
|
||||||
self.vault_popup = None
|
|
||||||
|
|
||||||
# Remove old secrets checkbox
|
# Remove old secrets checkbox
|
||||||
self.remove_old_checkbox = ctk.CTkCheckBox(
|
self.remove_old_checkbox = ctk.CTkCheckBox(
|
||||||
@@ -83,7 +71,7 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
text="Remove old secrets after creating new one",
|
text="Remove old secrets after creating new one",
|
||||||
font=ctk.CTkFont(size=14)
|
font=ctk.CTkFont(size=14)
|
||||||
)
|
)
|
||||||
self.remove_old_checkbox.grid(row=5, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="w")
|
self.remove_old_checkbox.grid(row=4, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="w")
|
||||||
|
|
||||||
# Generate button
|
# Generate button
|
||||||
self.generate_button = ctk.CTkButton(
|
self.generate_button = ctk.CTkButton(
|
||||||
@@ -94,7 +82,7 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
font=ctk.CTkFont(size=16, weight="bold"),
|
font=ctk.CTkFont(size=16, weight="bold"),
|
||||||
state="disabled"
|
state="disabled"
|
||||||
)
|
)
|
||||||
self.generate_button.grid(row=6, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
|
self.generate_button.grid(row=5, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
|
||||||
|
|
||||||
# Configure grid
|
# Configure grid
|
||||||
self.grid_columnconfigure(0, weight=1)
|
self.grid_columnconfigure(0, weight=1)
|
||||||
@@ -106,104 +94,12 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
Args:
|
Args:
|
||||||
vaults: List of vault dictionaries with 'name' and 'resource_group'
|
vaults: List of vault dictionaries with 'name' and 'resource_group'
|
||||||
"""
|
"""
|
||||||
self.vaults = vaults
|
self.vault_dropdown.set_items(vaults)
|
||||||
|
|
||||||
if vaults:
|
def _on_vault_selected(self, vault: Dict):
|
||||||
self.vault_dropdown_button.configure(state="normal")
|
"""Handle vault selection."""
|
||||||
# Auto-select first vault
|
# Selection is stored in dropdown - no additional action needed
|
||||||
self._select_vault(vaults[0])
|
pass
|
||||||
else:
|
|
||||||
self.vault_dropdown_button.configure(state="disabled", text="No Key Vaults found")
|
|
||||||
self.selected_vault = None
|
|
||||||
|
|
||||||
def _open_vault_dropdown(self):
|
|
||||||
"""Open the vault selection popup."""
|
|
||||||
if not self.vaults or self.vault_popup:
|
|
||||||
return
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
|
|
||||||
# Create popup
|
|
||||||
self.vault_popup = tk.Toplevel(self)
|
|
||||||
self.vault_popup.wm_overrideredirect(True)
|
|
||||||
self.vault_popup.wm_attributes("-topmost", True)
|
|
||||||
|
|
||||||
# Calculate height
|
|
||||||
item_height = 44
|
|
||||||
calculated_height = len(self.vaults) * item_height + 10
|
|
||||||
popup_height = min(calculated_height, 300)
|
|
||||||
|
|
||||||
# Create scrollable frame
|
|
||||||
scroll_frame = ctk.CTkScrollableFrame(
|
|
||||||
self.vault_popup,
|
|
||||||
width=580,
|
|
||||||
height=popup_height
|
|
||||||
)
|
|
||||||
scroll_frame.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
# Configure scroll speed to match main window (40px per scroll unit = 2x faster)
|
|
||||||
scroll_frame._parent_canvas.configure(yscrollincrement=40)
|
|
||||||
|
|
||||||
# Create buttons
|
|
||||||
for idx, vault in enumerate(self.vaults):
|
|
||||||
display_text = f"{vault['name']} (RG: {vault['resource_group']})"
|
|
||||||
max_chars = 60
|
|
||||||
truncated = display_text if len(display_text) <= max_chars else display_text[:max_chars] + "..."
|
|
||||||
|
|
||||||
btn = ctk.CTkButton(
|
|
||||||
scroll_frame,
|
|
||||||
text=truncated,
|
|
||||||
command=lambda v=vault: self._select_vault_and_close(v),
|
|
||||||
font=ctk.CTkFont(size=14),
|
|
||||||
height=40,
|
|
||||||
fg_color="transparent",
|
|
||||||
border_width=1,
|
|
||||||
border_color="gray50",
|
|
||||||
hover_color=("gray70", "gray30"),
|
|
||||||
anchor="w"
|
|
||||||
)
|
|
||||||
btn.grid(row=idx, column=0, padx=5, pady=2, sticky="ew")
|
|
||||||
scroll_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Bind mouse wheel
|
|
||||||
def popup_scroll(event):
|
|
||||||
if event.delta > 0:
|
|
||||||
scroll_frame._parent_canvas.yview_scroll(-1, "units")
|
|
||||||
else:
|
|
||||||
scroll_frame._parent_canvas.yview_scroll(1, "units")
|
|
||||||
return "break"
|
|
||||||
|
|
||||||
btn.bind("<MouseWheel>", popup_scroll, add="+")
|
|
||||||
|
|
||||||
# Position popup
|
|
||||||
self.vault_popup.update_idletasks()
|
|
||||||
x = self.vault_dropdown_button.winfo_rootx()
|
|
||||||
y = self.vault_dropdown_button.winfo_rooty() + self.vault_dropdown_button.winfo_height()
|
|
||||||
self.vault_popup.wm_geometry(f"+{x}+{y}")
|
|
||||||
|
|
||||||
# Bind events
|
|
||||||
self.vault_popup.bind("<Escape>", lambda e: self._close_vault_popup())
|
|
||||||
self.vault_popup.bind("<FocusOut>", lambda e: self._close_vault_popup())
|
|
||||||
self.vault_popup.focus_set()
|
|
||||||
|
|
||||||
def _select_vault(self, vault: Dict):
|
|
||||||
"""Select a vault and update button text."""
|
|
||||||
self.selected_vault = vault
|
|
||||||
display_text = f"{vault['name']} (RG: {vault['resource_group']})"
|
|
||||||
max_chars = 60
|
|
||||||
truncated = display_text if len(display_text) <= max_chars else display_text[:max_chars] + "..."
|
|
||||||
self.vault_dropdown_button.configure(text=truncated)
|
|
||||||
|
|
||||||
def _select_vault_and_close(self, vault: Dict):
|
|
||||||
"""Select vault and close popup."""
|
|
||||||
self._select_vault(vault)
|
|
||||||
self._close_vault_popup()
|
|
||||||
|
|
||||||
def _close_vault_popup(self):
|
|
||||||
"""Close the vault popup."""
|
|
||||||
if self.vault_popup:
|
|
||||||
self.vault_popup.destroy()
|
|
||||||
self.vault_popup = None
|
|
||||||
|
|
||||||
def _on_generate_clicked(self):
|
def _on_generate_clicked(self):
|
||||||
"""Handle generate button click."""
|
"""Handle generate button click."""
|
||||||
@@ -222,7 +118,7 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
Returns:
|
Returns:
|
||||||
Dict: Selected vault or None
|
Dict: Selected vault or None
|
||||||
"""
|
"""
|
||||||
return self.selected_vault
|
return self.vault_dropdown.get_selected()
|
||||||
|
|
||||||
def get_description(self) -> str:
|
def get_description(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -251,13 +147,12 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
"""
|
"""
|
||||||
if enabled:
|
if enabled:
|
||||||
self.description_entry.configure(state="normal")
|
self.description_entry.configure(state="normal")
|
||||||
if self.vaults:
|
self.vault_dropdown.set_enabled(True)
|
||||||
self.vault_dropdown_button.configure(state="normal")
|
|
||||||
self.remove_old_checkbox.configure(state="normal")
|
self.remove_old_checkbox.configure(state="normal")
|
||||||
self.generate_button.configure(state="normal")
|
self.generate_button.configure(state="normal")
|
||||||
else:
|
else:
|
||||||
self.description_entry.configure(state="disabled")
|
self.description_entry.configure(state="disabled")
|
||||||
self.vault_dropdown_button.configure(state="disabled")
|
self.vault_dropdown.set_enabled(False)
|
||||||
self.remove_old_checkbox.configure(state="disabled")
|
self.remove_old_checkbox.configure(state="disabled")
|
||||||
self.generate_button.configure(state="disabled")
|
self.generate_button.configure(state="disabled")
|
||||||
|
|
||||||
@@ -280,13 +175,7 @@ class SecretGenerationFrame(ctk.CTkFrame):
|
|||||||
Args:
|
Args:
|
||||||
loading: Whether currently loading
|
loading: Whether currently loading
|
||||||
"""
|
"""
|
||||||
if loading:
|
self.vault_dropdown.set_loading(loading)
|
||||||
self.vault_dropdown_button.configure(state="disabled", text="Loading Key Vaults...")
|
|
||||||
else:
|
|
||||||
if self.vaults:
|
|
||||||
self.vault_dropdown_button.configure(state="normal")
|
|
||||||
else:
|
|
||||||
self.vault_dropdown_button.configure(state="disabled", text="No Key Vaults found")
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Reset the form to initial state."""
|
"""Reset the form to initial state."""
|
||||||
|
|||||||
Reference in New Issue
Block a user