mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
UI: Standardized console appearance and fixed cosmetic inconsistencies. This commit establishes a stable, consistent UI across the entire installer: Unified 'Light Background, Dark Text' theme for all console components. Standardized console creation routine to eliminate thick black borders on macOS. Refactored SSH key generation to use the standard canvas-based layout. Switched to tk.Frame and tk.Scrollbar for TerminalConsole to resist macOS dark mode shifts. Fixed shutil.SameFileError when PROLE_HOME matches the source directory. Improved notebook styling for inactive tabs and execution outputs. All screens now share the same professional look and feel.
This commit is contained in:
parent
453ccfd015
commit
58d693de05
@ -221,7 +221,8 @@ initialize() {
|
||||
|
||||
# Ensure port-forward is running for OpenBao (dependency)
|
||||
echo "Ensuring port-forward for OpenBao is active ..."
|
||||
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
|
||||
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao &
|
||||
sleep 2
|
||||
|
||||
fetch_admin_keys_and_db_pass_from_bao_or_local
|
||||
apply_cnpg_admin_secret
|
||||
@ -231,7 +232,8 @@ initialize() {
|
||||
|
||||
# Ensure port-forward is running for Postgres (local access)
|
||||
echo "Ensuring port-forward for Postgres is active ..."
|
||||
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
|
||||
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres &
|
||||
sleep 2
|
||||
|
||||
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
|
||||
}
|
||||
|
||||
@ -459,13 +459,14 @@ case "$ACTION" in
|
||||
|
||||
if [[ -n "$db_pass" ]]; then
|
||||
username=${PROLE_DB_USER:-"prole"}
|
||||
echo "Creating database user secret 'prole-db-user' for '$username' ..."
|
||||
echo "Checking/creating database user secret 'prole-db-user' for '$username' ..."
|
||||
# Use apply to be idempotent
|
||||
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
|
||||
--from-literal=username="$username" \
|
||||
--from-literal=password="$db_pass" \
|
||||
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
|
||||
|
||||
echo "Creating database superuser secret 'prole-db-superuser' ..."
|
||||
echo "Checking/creating database superuser secret 'prole-db-superuser' ..."
|
||||
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
|
||||
--from-literal=username=postgres \
|
||||
--from-literal=password="$db_pass" \
|
||||
@ -476,13 +477,21 @@ case "$ACTION" in
|
||||
|
||||
# Ensure port-forward is running for OpenBao
|
||||
echo "Ensuring port-forward for OpenBao is active ..."
|
||||
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
|
||||
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao &
|
||||
sleep 2
|
||||
|
||||
if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
|
||||
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
|
||||
echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
|
||||
exit 1
|
||||
fi
|
||||
local max_retries=15
|
||||
local count=0
|
||||
while ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; do
|
||||
if [[ $count -ge $max_retries ]]; then
|
||||
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200 after waiting." >&2
|
||||
echo "Please check logs: $SCRIPT_DIR/init_port_forwards.sh status openbao" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for OpenBao port-forward to be ready... ($count/$max_retries)"
|
||||
sleep 2
|
||||
count=$((count + 1))
|
||||
done
|
||||
init_openbao_kv_and_store_admin_key
|
||||
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
|
||||
;;
|
||||
|
||||
359
install.py
359
install.py
@ -56,6 +56,12 @@ def get_resource_path(relative_path):
|
||||
return base_path / relative_path
|
||||
|
||||
|
||||
# Standard Console Theme (Light background, Dark text)
|
||||
CONSOLE_BG = '#F5F5DC' # Cream/Light background similar to our images
|
||||
CONSOLE_FG = '#1d1d1f' # Dark text
|
||||
CONSOLE_INSERT = '#1d1d1f'
|
||||
CONSOLE_FONT = ('Menlo', 10)
|
||||
|
||||
class ProleController:
|
||||
"""Business logic for Prole installer, separated from UI."""
|
||||
def __init__(self, project_root):
|
||||
@ -99,25 +105,28 @@ class ProleController:
|
||||
target_k8s = prole_home / "k8s"
|
||||
source_k8s = self.project_root / "k8s"
|
||||
if source_k8s.exists():
|
||||
if not target_k8s.exists() or (source_k8s.stat().st_mtime > target_k8s.stat().st_mtime):
|
||||
if target_k8s.exists():
|
||||
shutil.rmtree(target_k8s)
|
||||
shutil.copytree(source_k8s, target_k8s)
|
||||
if source_k8s.resolve() != target_k8s.resolve():
|
||||
if not target_k8s.exists() or (source_k8s.stat().st_mtime > target_k8s.stat().st_mtime):
|
||||
if target_k8s.exists():
|
||||
shutil.rmtree(target_k8s)
|
||||
shutil.copytree(source_k8s, target_k8s)
|
||||
|
||||
# Ensure conf/postgresql is copied for version detection
|
||||
target_conf = prole_home / "conf"
|
||||
source_conf = self.project_root / "conf"
|
||||
if source_conf.exists():
|
||||
if not target_conf.exists() or (source_conf.stat().st_mtime > target_conf.stat().st_mtime):
|
||||
# Only copy what we need or the whole thing? etc/ already exists in PROLE_HOME.
|
||||
# Let's copy the whole conf dir if it doesn't exist or is older.
|
||||
if target_conf.exists():
|
||||
shutil.rmtree(target_conf)
|
||||
shutil.copytree(source_conf, target_conf)
|
||||
if source_conf.resolve() != target_conf.resolve():
|
||||
if not target_conf.exists() or (source_conf.stat().st_mtime > target_conf.stat().st_mtime):
|
||||
# Only copy what we need or the whole thing? etc/ already exists in PROLE_HOME.
|
||||
# Let's copy the whole conf dir if it doesn't exist or is older.
|
||||
if target_conf.exists():
|
||||
shutil.rmtree(target_conf)
|
||||
shutil.copytree(source_conf, target_conf)
|
||||
|
||||
# Copy script and set permissions
|
||||
if source_script.exists():
|
||||
shutil.copy2(source_script, target_script)
|
||||
if source_script.resolve() != target_script.resolve():
|
||||
shutil.copy2(source_script, target_script)
|
||||
os.chmod(target_script, 0o755)
|
||||
|
||||
# Run from the installed location
|
||||
@ -129,6 +138,7 @@ class ProleController:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=0,
|
||||
env=env
|
||||
)
|
||||
|
||||
@ -197,7 +207,7 @@ class ProleInstaller:
|
||||
self.sidebar.pack_propagate(False)
|
||||
|
||||
# Vertical Divider Line
|
||||
self.divider = tk.Frame(self.main_container, bg='#CCCCCC', width=1)
|
||||
self.divider = tk.Frame(self.main_container, bg='#CCCCCC', width=1, highlightthickness=0, bd=0)
|
||||
self.divider.pack(side='left', fill='y')
|
||||
|
||||
# Right Content Area (approx 66%)
|
||||
@ -509,6 +519,23 @@ class ProleInstaller:
|
||||
bordercolor=[('active', '#E5E5D5'), ('pressed', '#D5D5C5')],
|
||||
lightcolor=[('active', '#E5E5D5'), ('pressed', '#D5D5C5')],
|
||||
darkcolor=[('active', '#E5E5D5'), ('pressed', '#D5D5C5')])
|
||||
|
||||
# Notebook styling to match light theme and avoid dark mode shifts on macOS
|
||||
self.style.theme_use('default')
|
||||
self.style.configure('TNotebook', background='white', borderwidth=0, highlightthickness=0)
|
||||
self.style.configure('TNotebook.Tab',
|
||||
background='#F5F5DC',
|
||||
foreground='black',
|
||||
lightcolor='#F5F5DC',
|
||||
bordercolor='#CCCCCC',
|
||||
darkcolor='#F5F5DC',
|
||||
borderwidth=1,
|
||||
padding=[10, 5])
|
||||
self.style.map('TNotebook.Tab',
|
||||
background=[('selected', 'white')],
|
||||
bordercolor=[('selected', '#CCCCCC')],
|
||||
lightcolor=[('selected', 'white')],
|
||||
focuscolor=[('selected', 'white')])
|
||||
|
||||
# Ensure common controls inherit white background
|
||||
try:
|
||||
@ -1068,8 +1095,25 @@ class ProleInstaller:
|
||||
def _render_paragraph(self, text, y, wrap=600):
|
||||
ui.render_paragraph(self, text, y, wrap)
|
||||
|
||||
def _create_console_output(self, y=280, title="Scan Output", width=880, height=450):
|
||||
"""Create a standardized console output area with a label and scrollable text."""
|
||||
# Section title/label for the console
|
||||
ui.canvas_text(self, 48, y, title, fill='#1d1d1f', font=('SF Pro Text', 12, 'bold'))
|
||||
|
||||
y_console = y + 30
|
||||
# Use a background frame to ensure NO borders are visible around the console
|
||||
console_bg = tk.Frame(self.bg_canvas, bg='white', highlightthickness=0, bd=0)
|
||||
console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0)
|
||||
console.pack(fill='both', expand=True, padx=1, pady=1)
|
||||
|
||||
console_window = self.bg_canvas.create_window(48, y_console, window=console_bg,
|
||||
anchor='nw', width=width, height=height)
|
||||
self._canvas_items.append(console_window)
|
||||
self._overlay_widgets.append(console_bg)
|
||||
self._overlay_widgets.append(console)
|
||||
return console
|
||||
|
||||
def _render_welcome_page(self):
|
||||
# Opening page should NOT perform any dependency checks.
|
||||
# Letterhead at top right
|
||||
content_width = self.bg_canvas.winfo_width() or 975 # 1300 * 0.75 approx
|
||||
right_margin = content_width - 48
|
||||
@ -1094,7 +1138,7 @@ class ProleInstaller:
|
||||
"Welcome to the neighborhood! Let's get started by preparing your system for the Prole experience."
|
||||
)
|
||||
|
||||
ui.canvas_text(self, 48, 210, welcome_text, fill='black', font=('SF Pro Text', 13), width=750)
|
||||
ui.canvas_text(self, 48, 220, welcome_text, fill='black', font=('SF Pro Text', 13), width=750)
|
||||
|
||||
# Ensure footer is updated (Next button visible)
|
||||
self.update_footer()
|
||||
@ -1124,34 +1168,11 @@ class ProleInstaller:
|
||||
pass
|
||||
self.scan_status_var.trace_add('write', update_status_text)
|
||||
|
||||
y += 40
|
||||
# Tabs for Scan Output and LLM Analysis
|
||||
self.scan_notebook = ttk.Notebook(self.bg_canvas)
|
||||
self.scan_tab = tk.Frame(self.scan_notebook, bg='white')
|
||||
self.analysis_tab = tk.Frame(self.scan_notebook, bg='white')
|
||||
# Standardized Console Output
|
||||
self.scan_results_console = self._create_console_output(y=320, title="Scan Output", width=880, height=380)
|
||||
self.scan_results_text = self.scan_results_console.text
|
||||
|
||||
self.scan_notebook.add(self.scan_tab, text="Scan Output")
|
||||
self.scan_notebook.add(self.analysis_tab, text="AI Analysis", state='disabled')
|
||||
|
||||
# Output Box (placed inside scan_tab)
|
||||
self.scan_results_text = scrolledtext.ScrolledText(self.scan_tab, width=110, height=22,
|
||||
font=('Menlo', 10), bg='white', fg='black',
|
||||
insertbackground='black', highlightthickness=1,
|
||||
highlightbackground='#CCCCCC')
|
||||
self.scan_results_text.pack(fill='both', expand=True, padx=5, pady=5)
|
||||
|
||||
# Analysis Box (placed inside analysis_tab)
|
||||
self.analysis_results_text = scrolledtext.ScrolledText(self.analysis_tab, width=110, height=22,
|
||||
font=('SF Pro Text', 11), bg='#F9F9F9', fg='black',
|
||||
insertbackground='black', highlightthickness=1,
|
||||
highlightbackground='#CCCCCC')
|
||||
self.analysis_results_text.pack(fill='both', expand=True, padx=5, pady=5)
|
||||
|
||||
notebook_window = self.bg_canvas.create_window(48, y, window=self.scan_notebook, anchor='nw', width=880, height=450)
|
||||
self._overlay_widgets.append(self.scan_notebook)
|
||||
self._canvas_items.append(notebook_window)
|
||||
|
||||
y += 480
|
||||
y = 740
|
||||
|
||||
# Start Network Scan Button (placed on canvas)
|
||||
self.scan_btn = tk.Button(self.bg_canvas, text="Start Network Scan", command=self._run_network_scan,
|
||||
@ -1166,7 +1187,7 @@ class ProleInstaller:
|
||||
if not GLOBAL_SCAN_FRAMES:
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
gif_path = PROJECT_ROOT / 'img' / 'prole-type.gif'
|
||||
gif_path = get_resource_path('img/prole-type.gif')
|
||||
if gif_path.exists():
|
||||
gif = Image.open(str(gif_path))
|
||||
try:
|
||||
@ -1202,12 +1223,8 @@ class ProleInstaller:
|
||||
return
|
||||
self._scan_running = True
|
||||
|
||||
# Reset tabs
|
||||
self.scan_notebook.tab(1, state='disabled')
|
||||
self.scan_notebook.select(0)
|
||||
|
||||
self.scan_results_text.delete('1.0', tk.END)
|
||||
self.scan_results_text.insert(tk.END, "Initializing network scan using prole-net/prole-scan ...\n")
|
||||
self.scan_results_console.clear()
|
||||
self.scan_results_console.write("Initializing network scan using prole-net/prole-scan ...\n")
|
||||
self.scan_status_var.set("Scanning...")
|
||||
|
||||
# Start animation
|
||||
@ -1235,7 +1252,7 @@ class ProleInstaller:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
bufsize=0,
|
||||
cwd=str(scan_dir))
|
||||
|
||||
# Capture output in real-time
|
||||
@ -1244,8 +1261,11 @@ class ProleInstaller:
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
self.root.after(0, lambda l=line: self.scan_results_text.insert(tk.END, l))
|
||||
self.root.after(0, lambda: self.scan_results_text.see(tk.END))
|
||||
# Use a method that forces text onto the console
|
||||
def _append(l):
|
||||
self.scan_results_console.write(l)
|
||||
self.scan_results_text.see(tk.END)
|
||||
self.root.after(0, lambda l=line: _append(l))
|
||||
|
||||
# Heuristic parsing for Kerberos/AD to auto-fill
|
||||
if "Active Directory" in line or "88" in line:
|
||||
@ -1273,7 +1293,7 @@ class ProleInstaller:
|
||||
self._scan_running = False
|
||||
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: self.scan_results_text.insert(tk.END, f"Scan error: {str(e)}\n"))
|
||||
self.root.after(0, lambda: self.scan_results_console.write(f"Scan error: {str(e)}\n"))
|
||||
self.root.after(0, lambda: self.scan_status_var.set("Scan failed"))
|
||||
self._scan_running = False
|
||||
|
||||
@ -1446,18 +1466,32 @@ class ProleInstaller:
|
||||
["kinit", principal],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=0
|
||||
)
|
||||
stdout, stderr = process.communicate(input=password + "\n")
|
||||
|
||||
# Send password if needed
|
||||
if password:
|
||||
process.stdin.write(password + "\n")
|
||||
process.stdin.close()
|
||||
|
||||
# Real-time output capture for Kerberos too
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
self.root.after(0, lambda l=line: self.kerberos_status_text.insert(tk.END, l))
|
||||
self.root.after(0, lambda: self.kerberos_status_text.see(tk.END))
|
||||
|
||||
if process.returncode == 0:
|
||||
self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, "Success! Authenticated successfully.\n"))
|
||||
# Get ticket details
|
||||
klist_res = subprocess.run(["klist"], capture_output=True, text=True)
|
||||
self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"\nTicket details:\n{klist_res.stdout}"))
|
||||
else:
|
||||
self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"Failed to authenticate.\nExit code: {process.returncode}\nError: {stderr}\n"))
|
||||
self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"Failed to authenticate.\nExit code: {process.returncode}\n"))
|
||||
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"Error: {str(e)}\n"))
|
||||
@ -1703,27 +1737,26 @@ class ProleInstaller:
|
||||
# ---------------- Initialize Screen Handlers ----------------
|
||||
|
||||
def _generate_ssh_key_with_overlay(self):
|
||||
"""Generate ed25519 SSH key pair with a status overlay."""
|
||||
# Create a semi-transparent overlay
|
||||
overlay_bg = ui.canvas_rectangle(self, 0, 0, 2000, 2000, fill='black', state='normal')
|
||||
self.bg_canvas.itemconfig(overlay_bg, stipple='gray50') # Approximation of transparency
|
||||
self._canvas_items.append(overlay_bg)
|
||||
|
||||
# Center console
|
||||
cw = self.bg_canvas.winfo_width() or 975
|
||||
ch = self.bg_canvas.winfo_height() or 800
|
||||
"""Generate ed25519 SSH key pair using a standard screen layout."""
|
||||
self._clear_canvas_page()
|
||||
|
||||
console_w, console_h = 700, 400
|
||||
x = (cw - console_w) // 2
|
||||
y = (ch - console_h) // 2
|
||||
# Letterhead at top right
|
||||
content_width = self.bg_canvas.winfo_width() or 975
|
||||
right_margin = content_width - 48
|
||||
|
||||
console = ui.TerminalConsole(self.bg_canvas)
|
||||
console_window = self.bg_canvas.create_window(x, y, window=console, anchor='nw', width=console_w, height=console_h)
|
||||
self._canvas_items.append(console_window)
|
||||
self._overlay_widgets.append(console)
|
||||
ui.canvas_text(self, right_margin, 40, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne')
|
||||
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73',
|
||||
font=('SF Pro Text', 18), anchor='ne')
|
||||
|
||||
status_text = ui.canvas_text(self, x, y - 30, "Generating SSH Key Pair...", fill='white', font=('SF Pro Text', 14, 'bold'))
|
||||
self._canvas_items.append(status_text)
|
||||
self._render_title('Generate SSH Key', y=150)
|
||||
self._render_paragraph('Generating ed25519 SSH key pair for secure database access.', y=200)
|
||||
|
||||
# Output Console - standardized to match Docker Build screen
|
||||
console = self._create_console_output(y=260, title="SSH Output", width=900, height=520)
|
||||
|
||||
# Status Label
|
||||
status_label = ui.canvas_text(self, 48, 812, "Initializing...", fill='black', font=('SF Pro Text', 12))
|
||||
self._canvas_items.append(status_label)
|
||||
|
||||
def worker():
|
||||
key_path = Path.home() / ".ssh" / "id_prole_ed25519"
|
||||
@ -1731,8 +1764,10 @@ class ProleInstaller:
|
||||
|
||||
if key_path.exists():
|
||||
console.write(f"Key already exists at {key_path}. Skipping generation.\n")
|
||||
time.sleep(1)
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="SSH key already exists. Proceeding...", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Generating key...", fill='blue') if self.bg_canvas.winfo_exists() else None)
|
||||
cmd = ["ssh-keygen", "-t", "ed25519", "-N", "", "-f", str(key_path), "-C", self.db_username.get()]
|
||||
console.write(f"Running: {' '.join(cmd)}\n\n")
|
||||
|
||||
@ -1746,8 +1781,10 @@ class ProleInstaller:
|
||||
|
||||
if proc.returncode == 0:
|
||||
console.write("\nSSH key generated successfully.\n")
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="SSH key generated successfully.", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
|
||||
else:
|
||||
console.write(f"\nError generating SSH key (code {proc.returncode})\n")
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text=f"Error generating key (code {proc.returncode})", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
|
||||
# In case of error, we might want to let the user see it before continuing or stopping
|
||||
time.sleep(2)
|
||||
|
||||
@ -1907,10 +1944,7 @@ class ProleInstaller:
|
||||
self._render_paragraph('Building the prole-db Postgres image. This may take a few minutes.', y=200)
|
||||
|
||||
# Output Console
|
||||
self._db_build_console = ui.TerminalConsole(self.bg_canvas)
|
||||
console_window = self.bg_canvas.create_window(48, 260, window=self._db_build_console, anchor='nw', width=900, height=520)
|
||||
self._canvas_items.append(console_window)
|
||||
self._overlay_widgets.append(self._db_build_console)
|
||||
self._db_build_console = self._create_console_output(y=260, title="Build Output", width=900, height=520)
|
||||
|
||||
# Use tk.Button
|
||||
self._db_build_button = tk.Button(self.bg_canvas, text='Start Build', command=self.run_db_build,
|
||||
@ -1953,9 +1987,10 @@ class ProleInstaller:
|
||||
if source_dir.exists():
|
||||
import shutil
|
||||
# Remove old build dir and copy fresh
|
||||
if build_dir.exists():
|
||||
shutil.rmtree(build_dir)
|
||||
shutil.copytree(source_dir, build_dir)
|
||||
if source_dir.resolve() != build_dir.resolve():
|
||||
if build_dir.exists():
|
||||
shutil.rmtree(build_dir)
|
||||
shutil.copytree(source_dir, build_dir)
|
||||
|
||||
cwd = build_dir
|
||||
|
||||
@ -2079,10 +2114,7 @@ class ProleInstaller:
|
||||
self._render_paragraph('Deploying the CloudNative-PG operator and cluster manifests to Kubernetes.', y=200)
|
||||
|
||||
# Output Console
|
||||
self._cnpg_deploy_console = ui.TerminalConsole(self.bg_canvas)
|
||||
console_window = self.bg_canvas.create_window(48, 260, window=self._cnpg_deploy_console, anchor='nw', width=900, height=520)
|
||||
self._canvas_items.append(console_window)
|
||||
self._overlay_widgets.append(self._cnpg_deploy_console)
|
||||
self._cnpg_deploy_console = self._create_console_output(y=260, title="Deployment Output", width=900, height=520)
|
||||
|
||||
# Use tk.Button
|
||||
self._cnpg_deploy_button = tk.Button(self.bg_canvas, text='Run Deployment', command=self.run_cnpg_deploy,
|
||||
@ -2191,10 +2223,17 @@ class ProleInstaller:
|
||||
self._render_title('Initialization Scripts', y=150)
|
||||
self._render_paragraph('Running initialization scripts to set up OpenBao, CloudNative-PG, and Port Forwards.', y=200)
|
||||
|
||||
# Tabs for output
|
||||
self.script_tabs = ttk.Notebook(self.bg_canvas)
|
||||
tab_window = self.bg_canvas.create_window(48, 260, window=self.script_tabs, anchor='nw', width=900, height=480)
|
||||
# Tabs for output - using standardized appearance
|
||||
ui.canvas_text(self, 48, 260, "Execution Output", fill='#1d1d1f', font=('SF Pro Text', 12, 'bold'))
|
||||
|
||||
# Use a background frame for the notebook to hide potential system borders
|
||||
notebook_bg = tk.Frame(self.bg_canvas, bg='white', highlightthickness=0, bd=0)
|
||||
self.script_tabs = ttk.Notebook(notebook_bg, style='TNotebook')
|
||||
self.script_tabs.pack(fill='both', expand=True, padx=1, pady=1)
|
||||
|
||||
tab_window = self.bg_canvas.create_window(48, 290, window=notebook_bg, anchor='nw', width=900, height=450)
|
||||
self._canvas_items.append(tab_window)
|
||||
self._overlay_widgets.append(notebook_bg)
|
||||
self._overlay_widgets.append(self.script_tabs)
|
||||
|
||||
self.script_consoles = {}
|
||||
@ -2208,10 +2247,12 @@ class ProleInstaller:
|
||||
]
|
||||
|
||||
for title, fname in scripts:
|
||||
frame = tk.Frame(self.script_tabs, bg='white')
|
||||
self.script_tabs.add(frame, text=title)
|
||||
console = ui.TerminalConsole(frame)
|
||||
console.pack(fill='both', expand=True)
|
||||
# Use a background frame to ensure NO borders are visible around the console
|
||||
console_bg = tk.Frame(self.script_tabs, bg='white', highlightthickness=0, bd=0)
|
||||
self.script_tabs.add(console_bg, text=title)
|
||||
# Use TerminalConsole for consistent styling
|
||||
console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0)
|
||||
console.pack(fill='both', expand=True, padx=1, pady=1)
|
||||
self.script_consoles[fname] = console
|
||||
|
||||
# Use tk.Button
|
||||
@ -2222,7 +2263,7 @@ class ProleInstaller:
|
||||
btn_window = self.bg_canvas.create_window(48, 760, window=self._init_scripts_button, anchor='nw', width=180)
|
||||
self._canvas_items.append(btn_window)
|
||||
self._overlay_widgets.append(self._init_scripts_button)
|
||||
|
||||
|
||||
# Status Label
|
||||
self._init_scripts_status_label = ui.canvas_text(self, 240, 772, "", fill='black', font=('SF Pro Text', 12))
|
||||
self._canvas_items.append(self._init_scripts_status_label)
|
||||
@ -2385,6 +2426,7 @@ class ProleInstaller:
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Initialization failed.", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
|
||||
self._scripts_success = False
|
||||
|
||||
self.safe_after(lambda: self.update_footer())
|
||||
self.safe_after(lambda: self._init_scripts_button.configure(state='normal') if self._init_scripts_button.winfo_exists() else None)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
@ -2590,10 +2632,7 @@ class ProleInstaller:
|
||||
self.bg_canvas.bind('<Button-1>', _on_click)
|
||||
|
||||
# Output Console (Consistent size and location: y=260, width=900, height=520)
|
||||
self._build_console = ui.TerminalConsole(self.bg_canvas)
|
||||
console_window = self.bg_canvas.create_window(48, 260, window=self._build_console, anchor='nw', width=900, height=520)
|
||||
self._canvas_items.append(console_window)
|
||||
self._overlay_widgets.append(self._build_console)
|
||||
self._build_console = self._create_console_output(y=230, title="Build Output", width=900, height=550)
|
||||
self._console_text = self._build_console.text # For compatibility with _append_console and others
|
||||
|
||||
# Show a command preview with PS1-style prompt and blinking cursor
|
||||
@ -2884,6 +2923,12 @@ class ProleInstaller:
|
||||
self.next_button.configure(text='Next')
|
||||
else:
|
||||
self.next_button.configure(text='Build')
|
||||
elif pid == 'init_scripts':
|
||||
# Initialization Scripts: Next is disabled until success
|
||||
if getattr(self, '_scripts_success', False):
|
||||
self.next_button.configure(state='normal')
|
||||
else:
|
||||
self.next_button.configure(state='disabled')
|
||||
elif pid == 'create_installer':
|
||||
self.next_button.configure(text='Finish')
|
||||
|
||||
@ -3140,8 +3185,12 @@ class ProleInstaller:
|
||||
ttk.Combobox(wrap, textvariable=self.deploy_environment, values=['Dev', 'Service', 'Prod'], state='readonly', width=18).pack(side='left', padx=10)
|
||||
|
||||
# Build output console
|
||||
self.build_output = scrolledtext.ScrolledText(f, height=12, bg='#fafafa', fg='#1d1d1f')
|
||||
self.build_output.pack(fill='both', expand=True, padx=24, pady=12)
|
||||
# Use a background frame to ensure NO borders are visible around the console
|
||||
console_bg = tk.Frame(f, bg='white', highlightthickness=0, bd=0)
|
||||
console_bg.pack(fill='both', expand=True, padx=24, pady=12)
|
||||
self.build_output_console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0)
|
||||
self.build_output_console.pack(fill='both', expand=True, padx=1, pady=1)
|
||||
self.build_output = self.build_output_console.text
|
||||
|
||||
# self._register_page('build', f)
|
||||
|
||||
@ -3368,7 +3417,7 @@ echo "-------------------------------------------------------------------";
|
||||
|
||||
# ---------------- Embedded console helpers ----------------
|
||||
def _ensure_console_overlay(self, radio_bottom_y: int = 160):
|
||||
"""Create semi-transparent black backdrop and a ScrolledText console overlay.
|
||||
"""Create semi-transparent backdrop and a ScrolledText console overlay.
|
||||
The overlay is placed within slide_area between given top and bottom margins.
|
||||
"""
|
||||
# Ensure slide_area is visible
|
||||
@ -3378,30 +3427,23 @@ echo "-------------------------------------------------------------------";
|
||||
# Compute geometry within slide area
|
||||
geom = self._compute_console_geometry(radio_bottom_y)
|
||||
left, top, width, height = geom
|
||||
# Draw a stippled rectangle on the canvas to simulate ~60% opacity
|
||||
rect = ui.canvas_rectangle(self, left, top, left + width, top + height, fill='#000000', outline='')
|
||||
try:
|
||||
# Apply stipple directly on the canvas item if supported
|
||||
self.bg_canvas.itemconfig(rect, stipple='gray50')
|
||||
except Exception:
|
||||
pass
|
||||
self._canvas_items.append(rect)
|
||||
# Create ScrolledText overlay
|
||||
txt = scrolledtext.ScrolledText(self.slide_area, wrap='word',
|
||||
bg='#000000', fg='#ffffff', insertbackground='#ffffff',
|
||||
font=('Menlo', 11), relief='flat', bd=0, highlightthickness=0)
|
||||
txt.place(x=left + 8, y=top + 8, width=max(50, width - 16), height=max(50, height - 16))
|
||||
self._overlay_widgets.append(txt)
|
||||
self._console_text = txt
|
||||
|
||||
# Use a background frame to ensure NO borders are visible around the console
|
||||
console_bg = tk.Frame(self.slide_area, bg='white', highlightthickness=0, bd=0)
|
||||
console_bg.place(x=left, y=top, width=width, height=height)
|
||||
|
||||
# Create TerminalConsole overlay
|
||||
console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0)
|
||||
console.pack(fill='both', expand=True, padx=1, pady=1)
|
||||
self._overlay_widgets.append(console_bg)
|
||||
self._overlay_widgets.append(console)
|
||||
self._console_text = console.text
|
||||
|
||||
# Keep overlay positioned on resize
|
||||
def _on_resize(_evt=None):
|
||||
l, t, w, h = self._compute_console_geometry(radio_bottom_y)
|
||||
try:
|
||||
self.bg_canvas.coords(rect, l, t, l + w, t + h)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
txt.place(x=l + 8, y=t + 8, width=max(50, w - 16), height=max(50, h - 16))
|
||||
console_bg.place(x=l, y=t, width=w, height=h)
|
||||
except Exception:
|
||||
pass
|
||||
# Bind to slide area; store bind id to unbind later
|
||||
@ -3922,38 +3964,30 @@ echo "-------------------------------------------------------------------";
|
||||
)
|
||||
self._render_paragraph(tips, y=88)
|
||||
logp = getattr(self, 'last_build_log_path', None)
|
||||
# Place a scrolled text overlay to show the log
|
||||
# Place a scrollable console to show the log
|
||||
try:
|
||||
# Use same geometry helper as console but with DARK theme (match Build console)
|
||||
l, t, w, h = self._compute_console_geometry(160)
|
||||
txt = scrolledtext.ScrolledText(self.slide_area, wrap='word',
|
||||
bg='#000000', fg='#ffffff', insertbackground='#ffffff',
|
||||
font=('Menlo', 11), relief='flat', bd=0, highlightthickness=0)
|
||||
txt.place(x=l, y=t, width=max(200, w), height=max(140, h))
|
||||
self._overlay_widgets.append(txt)
|
||||
# Standardized Console Output area for build summary
|
||||
self.build_summary_console = self._create_console_output(y=160, title="Build Log Output", width=900, height=520)
|
||||
txt = self.build_summary_console.text
|
||||
|
||||
if logp and os.path.exists(logp):
|
||||
try:
|
||||
with open(logp, 'r', encoding='utf-8', errors='ignore') as fp:
|
||||
content = fp.read()
|
||||
txt.insert('1.0', content)
|
||||
self.build_summary_console.write(content)
|
||||
except Exception as e:
|
||||
txt.insert('1.0', f"Failed to read log: {e}\n")
|
||||
self.build_summary_console.write(f"Failed to read log: {e}\n")
|
||||
else:
|
||||
txt.insert('1.0', 'No build log available.')
|
||||
txt.configure(state='disabled')
|
||||
# Keep overlay positioned on resize
|
||||
def _on_resize(_evt=None):
|
||||
try:
|
||||
l2, t2, w2, h2 = self._compute_console_geometry(160)
|
||||
txt.place(x=l2, y=t2, width=max(200, w2), height=max(140, h2))
|
||||
except Exception:
|
||||
pass
|
||||
self._overlay_bind_id = self.slide_area.bind('<Configure>', _on_resize)
|
||||
self.build_summary_console.write('No build log available.')
|
||||
|
||||
# Keep standard y for links
|
||||
y_links = 760
|
||||
except Exception:
|
||||
# Fallback: just show path
|
||||
self._render_paragraph('No build log could be displayed.', y=120)
|
||||
y_links = 140
|
||||
|
||||
# Add link to open the log file in Finder/TextEdit
|
||||
y_links = 100 + 40
|
||||
if logp:
|
||||
link = ui.render_link(self, 56, y_links, 'Open build log file')
|
||||
self._canvas_items.append(link)
|
||||
@ -5157,7 +5191,7 @@ esac
|
||||
|
||||
def create_validate_screen(self):
|
||||
"""Create the Validate screen"""
|
||||
frame = tk.Frame(self.content_area, bg='#1a1a1a')
|
||||
frame = self._page_container()
|
||||
self.screens['validate'] = frame
|
||||
|
||||
# Title
|
||||
@ -5165,19 +5199,19 @@ esac
|
||||
title.pack(pady=(0, 20))
|
||||
|
||||
# Prometheus link
|
||||
prometheus_frame = tk.Frame(frame, bg='#1a1a1a')
|
||||
prometheus_frame = tk.Frame(frame, bg='white')
|
||||
prometheus_frame.pack(pady=(0, 20))
|
||||
|
||||
prometheus_label = tk.Label(prometheus_frame,
|
||||
text="Prometheus: ",
|
||||
bg='#1a1a1a',
|
||||
fg='#aaaaaa',
|
||||
bg='white',
|
||||
fg='#6e6e73',
|
||||
font=('Helvetica', 11))
|
||||
prometheus_label.pack(side='left')
|
||||
|
||||
prometheus_link = tk.Label(prometheus_frame,
|
||||
text="http://localhost:9090",
|
||||
bg='#1a1a1a',
|
||||
bg='white',
|
||||
fg='#4a9eff',
|
||||
font=('Helvetica', 11, 'underline'),
|
||||
cursor='hand2')
|
||||
@ -5185,35 +5219,33 @@ esac
|
||||
prometheus_link.bind('<Button-1>', lambda e: webbrowser.open('http://localhost:9090'))
|
||||
|
||||
# Status display
|
||||
status_frame = tk.Frame(frame, bg='#1a1a1a')
|
||||
status_frame = tk.Frame(frame, bg='white')
|
||||
status_frame.pack(fill='both', expand=True, pady=10)
|
||||
|
||||
status_label = ttk.Label(status_frame, text="Cluster Status", style='Heading.TLabel')
|
||||
status_label.pack(anchor='w', pady=(0, 10))
|
||||
|
||||
# Status text area
|
||||
self.status_text = scrolledtext.ScrolledText(status_frame,
|
||||
bg='#0a0a0a',
|
||||
fg='#4a9eff',
|
||||
font=('Courier', 10),
|
||||
wrap='word',
|
||||
relief='flat',
|
||||
bd=1)
|
||||
self.status_text.pack(fill='both', expand=True)
|
||||
# Status text area - using TerminalConsole for consistency
|
||||
# Use a background frame to ensure NO borders are visible around the console
|
||||
console_bg = tk.Frame(status_frame, bg='white', highlightthickness=0, bd=0)
|
||||
console_bg.pack(fill='both', expand=True, padx=1, pady=1)
|
||||
self._validation_console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0)
|
||||
self._validation_console.pack(fill='both', expand=True)
|
||||
self.status_text = self._validation_console.text
|
||||
|
||||
# Auto-refresh checkbox
|
||||
refresh_frame = tk.Frame(frame, bg='#1a1a1a')
|
||||
refresh_frame = tk.Frame(frame, bg='white')
|
||||
refresh_frame.pack(pady=10)
|
||||
|
||||
self.auto_refresh_var = tk.BooleanVar(value=True)
|
||||
refresh_check = tk.Checkbutton(refresh_frame,
|
||||
text="Auto-refresh every 10 seconds",
|
||||
variable=self.auto_refresh_var,
|
||||
bg='#1a1a1a',
|
||||
fg='#aaaaaa',
|
||||
selectcolor='#2a2a2a',
|
||||
activebackground='#1a1a1a',
|
||||
activeforeground='#aaaaaa',
|
||||
bg='white',
|
||||
fg='#1d1d1f',
|
||||
selectcolor='#f0f0f0',
|
||||
activebackground='white',
|
||||
activeforeground='#1d1d1f',
|
||||
font=('Helvetica', 10),
|
||||
command=self.toggle_auto_refresh)
|
||||
refresh_check.pack(side='left', padx=10)
|
||||
@ -5225,6 +5257,7 @@ esac
|
||||
bg='#4a9eff',
|
||||
fg='white',
|
||||
activebackground='#3a8eef',
|
||||
highlightbackground='white',
|
||||
font=('Helvetica', 10),
|
||||
padx=15,
|
||||
pady=5,
|
||||
|
||||
@ -191,20 +191,31 @@ def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int
|
||||
return btn_map
|
||||
|
||||
|
||||
class TerminalConsole(ttk.Frame):
|
||||
class TerminalConsole(tk.Frame):
|
||||
"""A scrollable text widget that mimics a terminal console."""
|
||||
def __init__(self, parent, **kwargs):
|
||||
bg = '#F5F5DC' # Cream/Light background
|
||||
kwargs.setdefault('bg', bg)
|
||||
super().__init__(parent, **kwargs)
|
||||
self.text = tk.Text(self, bg='#1e1e1e', fg='#f0f0f0', font=('Menlo', 11) if 'Darwin' in platform.system() else ('Consolas', 11),
|
||||
padx=10, pady=10, insertbackground='white')
|
||||
self.scroll = ttk.Scrollbar(self, orient='vertical', command=self.text.yview)
|
||||
# Standard Console Theme (Light background, Dark text)
|
||||
fg = '#1d1d1f' # Dark text
|
||||
font = ('Menlo', 10) if 'Darwin' in platform.system() else ('Consolas', 10)
|
||||
|
||||
self.text = tk.Text(self, bg=bg, fg=fg, font=font,
|
||||
padx=10, pady=10, insertbackground=fg,
|
||||
highlightthickness=0, bd=0, relief='flat')
|
||||
|
||||
# Force a light scrollbar style on macOS to avoid dark mode shifts
|
||||
self.scroll = tk.Scrollbar(self, orient='vertical', command=self.text.yview,
|
||||
highlightthickness=0, bd=0, bg=bg,
|
||||
activebackground=bg, troughcolor=bg)
|
||||
self.text.configure(yscrollcommand=self.scroll.set)
|
||||
|
||||
self.scroll.pack(side='right', fill='y')
|
||||
self.text.pack(side='left', fill='both', expand=True)
|
||||
|
||||
# Make read-only by default but allow selection and copying
|
||||
self.text.configure(state='disabled')
|
||||
self.text.configure(state='disabled', relief='flat')
|
||||
|
||||
def write(self, content: str):
|
||||
"""Append text to the console and scroll to the bottom."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user