##########################################################################
# Copyright (c) 2024, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
#
# DISCLAIMER This is not an official Oracle application, It does not supported by Oracle Support.
#
# settings_tab.py
#
# @author: Andrew Gregory
#
# Supports Python 3.12 and above
#
# coding: utf-8
##########################################################################
import os
import time
import tkinter as tk
import webbrowser
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from oci_policy_analysis.common import config
from oci_policy_analysis.common.caching import CacheManager
from oci_policy_analysis.common.logger import get_logger
from oci_policy_analysis.logic.ai_repo import AI
from oci_policy_analysis.ui.base_tab import BaseUITab
from oci_policy_analysis.ui.data_table import DataTable
# Constants for data table
AI_MODEL_COLUMNS = ['Model Name', 'Model OCID', 'Lifecycle State', 'Creation Date']
AI_MODEL_COLUMN_WIDTHS = {'Model Name': 250, 'Model OCID': 450, 'Lifecycle State': 125, 'Creation Date': 250}
# Context help messages for the SettingsTab
CONTEXT_HELP = {
'DISPLAY_OPTIONS': 'Adjust display, font size, and power-user tab features. Affects the entire UI experience.',
'MCP_CONFIG': 'Configure the built-in MCP server for advanced automation and API analysis integration.',
'TENANCY_CONFIG': (
'Set your OCI tenancy and authentication method. Choose between instance principal, named profile, or session token. '
'Load data either directly from OCI or from a cached JSON, manage policy data, and import/export backups. '
'Use these controls to initialize or refresh the core analysis dataset.'
),
'INSTANCE_PRINCIPAL': 'Use when running from OCI Compute with permissions. No config needed.',
'LOAD_ALL_USERS': 'If checked, loads all users for analysis. Uncheck for groups-only mode.',
'RECURSIVE_LOAD': 'If checked, load policies from all compartments.\nUncheck for root compartment only.',
'CACHE_LABEL': "A (P) before a cache means it is 'preserved' and will not be automatically overwritten or deleted. To update or manage the cache list, use the Maintenance Tab.",
'CACHE_DROPDOWN': "A (P) before a cache means it is 'preserved' and will not be automatically overwritten or deleted. To update or manage the cache list, use the Maintenance Tab.",
'SESSION_TOKEN': "Paste temporary token from 'oci session authenticate'. Use for cloud OCI CLI auth.",
'COMPLIANCE_OUTPUT': (
'Load from the output of an unzipped CIS Complaince run - these scripts are provided by Oracle as part of MAP engagement, '
'or freely downloadable from https://github.com/oci-landing-zones/oci-cis-landingzone-quickstart/blob/main/README.md'
),
'OCI_GENAI': (
'Set up connectivity to Oracle’s Generative AI services. Select or refresh models, test endpoints and compartments, and verify access. '
'Use this panel to enable policy text analysis and AI-driven explanations.'
),
}
# Global logger for this module
logger = get_logger(component='settings_tab')
[docs]
class SettingsTab(BaseUITab):
"""
Settings Tab for OCI Policy Analysis UI.
Allows configuration of tenancy, profile, MCP server, and AI settings.
All tenancy data is loaded via this tab.
"""
def __init__(self, parent, app, caching: CacheManager, ai_repo: AI, settings):
"""Initialize Settings Tab UI. Everything that the tab needs to exist in the notebook-based app."""
super().__init__(
parent,
default_help_text='Manage core settings for the OCI Policy Analysis tool, including tenancy authentication, caching, MCP server configuration, GenAI options, and general UI preferences.',
)
self.app = app
self.settings = settings
self.ai_repo = ai_repo
self.caching = caching
self.page_help_text = self.default_help_text
# Remove redundant page_help_frame, label, and methods; now in base
# Set the options from the saved settings
self.tenancy_var = tk.StringVar(value=self.settings.get('tenancy_ocid', ''))
self.profile_var = tk.StringVar(value=self.settings.get('named_profile', ''))
self.recursive_var = tk.BooleanVar(value=self.settings.get('recursive', True))
self.ip_var = tk.BooleanVar(value=self.settings.get('instance_principal', False))
self.context_help_var = tk.BooleanVar(value=self.settings.get('context_help', True))
# ----- NEW: Load All Users Option -----
self.load_all_users_var = tk.BooleanVar(value=self.settings.get('load_all_users', True))
self.ai_compartment_var = tk.StringVar(
value=self.settings.get('ai_compartment_ocid', '<use compartment or tenancy ocid with genai permission>')
)
self.format_var = tk.StringVar(value=self.settings.get('result_format', 'Markdown'))
self.mcp_port_var = tk.StringVar(value=str(self.settings.get('mcp_port', '8765')))
self.mcp_host_var = tk.StringVar(value=self.settings.get('mcp_host', '127.0.0.1'))
# Load profiles from ~/.oci/config
self.profile_list = ['DEFAULT']
try:
# TODO - Check Env OCI_CLI_CONFIG_FILE
with open(Path.home() / '.oci' / 'config') as fp:
self.profile_list = [line[1:-2] for line in fp if line.startswith('[') and line.endswith(']\n')]
except FileNotFoundError:
logger.warning('OCI config file not found - default to instance principal only')
self.profile_list = []
self.ip_var.set(True)
# Build the UI
self._build_ui()
# ----- Page Help (Context Help) helpers -----
# (Now inherited from BaseUITab. Any customizations can override as needed.)
def _build_ui(self): # noqa: C901
# ---- Top Row: Display + MCP ----
top_row = ttk.Frame(self)
top_row.pack(fill='x', padx=10, pady=10)
# Display options (LabelFrame, LEFT)
disp = ttk.LabelFrame(top_row, text='Display Options')
disp.pack(side='left', fill='both', expand=True, padx=(0, 8), pady=0)
# --- Page Help context for Display Options ---
self.add_context_help(disp, CONTEXT_HELP['DISPLAY_OPTIONS'])
# MCP Config (LabelFrame, RIGHT of display)
label_frm_mcp_config = ttk.LabelFrame(top_row, text='Embedded MCP')
label_frm_mcp_config.pack(side='left', fill='both', expand=True)
self.add_context_help(
label_frm_mcp_config,
CONTEXT_HELP['MCP_CONFIG'],
)
# Font size (in Display Panel)
ttk.Label(disp, text='Font Size:').pack(side='left', padx=(8, 4))
self.font_var = tk.StringVar(value=self.settings.get('font_size', 'Medium'))
font_combo = ttk.Combobox(
disp,
textvariable=self.font_var,
values=['Small', 'Medium', 'Large', 'Extra Large'],
state='readonly',
width=10,
)
font_combo.pack(side='left')
# Instead of direct apply_theme,
# call App.apply_theme, which itself triggers refresh_all_tabs_settings.
font_combo.bind('<<ComboboxSelected>>', self.app.apply_theme)
# --- Context Help Checkbox ---
self.context_help_check = ttk.Checkbutton(
disp,
text='Context Help',
variable=self.context_help_var,
command=self._on_context_help_changed,
)
self.context_help_check.pack(side='left', padx=10, pady=6)
# --- Console / Debug Button ---
self.console_btn_var = tk.StringVar(value='Show Console and Debug Tab')
self.console_button = ttk.Button(disp, textvariable=self.console_btn_var, command=self._toggle_console_tab)
self.console_button.pack(side='left', padx=10, pady=6)
# --- Maintenance Button next to Console ---
self.maintenance_btn_var = tk.StringVar(value='Show Maintenance Tab')
self.maintenance_button = ttk.Button(
disp, textvariable=self.maintenance_btn_var, command=self._toggle_maintenance_tab
)
self.maintenance_button.pack(side='left', padx=10, pady=6)
# --- Advanced Tabs Button ---
self.advanced_btn_var = tk.StringVar(value='Show Advanced Tabs')
self.advanced_button = ttk.Button(disp, textvariable=self.advanced_btn_var, command=self._toggle_advanced_tabs)
self.advanced_button.pack(side='left', padx=10, pady=6)
# --- MCP Configuration (RIGHT of Display Options) ---
ttk.Label(label_frm_mcp_config, text='Host:').grid(row=0, column=0, sticky=tk.W, padx=5, pady=3)
ttk.Entry(label_frm_mcp_config, textvariable=self.mcp_host_var, width=20).grid(
row=0, column=1, sticky=tk.W, padx=5
)
ttk.Label(label_frm_mcp_config, text='Port:').grid(row=0, column=2, sticky=tk.W, padx=5)
ttk.Entry(label_frm_mcp_config, textvariable=self.mcp_port_var, width=10).grid(
row=0, column=3, sticky=tk.W, padx=5
)
# Autosave for Host and Port
def autosave_mcp_var(*args):
try:
port_val = int(self.mcp_port_var.get())
except ValueError:
# Don't autosave until a valid integer - could add error state if desired
return
self.settings['mcp_port'] = port_val
self.settings['mcp_host'] = self.mcp_host_var.get().strip() or '127.0.0.1'
config.save_settings(self.settings)
logger.info('MCP configuration AUTOSAVED.')
self.mcp_host_var.trace_add('write', autosave_mcp_var)
self.mcp_port_var.trace_add('write', autosave_mcp_var)
# Remove Save Config button (no longer needed)
# --- Tenancy Config (LabelFrame) ---
label_frm_tenancy_config = ttk.Labelframe(self, text='Tenancy and Config')
label_frm_tenancy_config.pack(fill='x', padx=10, pady=10)
# --- Page Help context for Tenancy and Config ---
def _show_tenancy_help(_event=None):
self.set_page_help_text(CONTEXT_HELP['TENANCY_CONFIG'])
label_frm_tenancy_config.bind('<Enter>', _show_tenancy_help)
label_frm_tenancy_config.bind('<Leave>', lambda e=None: self.set_page_help_text(self.default_help_text))
# --- Checkbox group: Instance Principal, Load All Users, Recursive ---
frm_chkboxes = ttk.Frame(label_frm_tenancy_config)
frm_chkboxes.grid(row=0, column=0, rowspan=2, padx=(0, 2), pady=(0, 1), sticky='nw')
# Instance Principal checkbox
chk_instance_principal = ttk.Checkbutton(
frm_chkboxes,
text='Instance Principal',
variable=self.ip_var,
)
chk_instance_principal.grid(row=0, column=0, padx=4, pady=(2, 2), sticky='w')
self.add_context_help(chk_instance_principal, CONTEXT_HELP['INSTANCE_PRINCIPAL'])
# Load All Users checkbox
self.load_all_users_check = ttk.Checkbutton(
frm_chkboxes,
text='Load All Users',
variable=self.load_all_users_var,
command=self._on_load_all_users_changed,
)
self.load_all_users_check.grid(row=1, column=0, padx=4, pady=(2, 2), sticky='w')
self.add_context_help(self.load_all_users_check, CONTEXT_HELP['LOAD_ALL_USERS'])
# Recursion checkbox
self.recursive_load = ttk.Checkbutton(
frm_chkboxes,
text='Recursive',
variable=self.recursive_var,
)
self.recursive_load.grid(row=2, column=0, padx=4, pady=(2, 2), sticky='w')
self.add_context_help(self.recursive_load, CONTEXT_HELP['RECURSIVE_LOAD'])
# Profile Selection
self.label_profile = ttk.Label(label_frm_tenancy_config, text='Profile:')
self.label_profile.grid(row=0, column=1, padx=5, pady=3)
self.input_profile = ttk.OptionMenu(
label_frm_tenancy_config, self.profile_var, self.profile_var.get(), *self.profile_list
)
self.input_profile.config(width=20, state='normal' if len(self.profile_list) > 0 else 'disabled')
self.input_profile.grid(row=0, column=2, padx=5, pady=3)
# Get available cached copies
self.label_cache = ttk.Label(label_frm_tenancy_config, text='Cache:')
self.label_cache.grid(row=1, column=1, padx=5, pady=3)
self.add_context_help(self.label_cache, CONTEXT_HELP['CACHE_LABEL'])
self.cache_list = self.caching.get_available_cache(None)
# Use preserved_caches set from CacheManager for cache list display
preserved_caches = self.caching.get_preserved_cache_set()
self.cache_list_display = [f'(P) {name}' if name in preserved_caches else name for name in self.cache_list]
# Mapping: display -> actual cache key
self.display_to_cache_key = {
f'(P) {name}' if name in preserved_caches else name: name for name in self.cache_list
}
self.cache_var = tk.StringVar(
value=self.cache_list_display[0] if len(self.cache_list_display) > 0 else 'No Cache Available'
)
self.cache_list_dropdown = ttk.OptionMenu(
label_frm_tenancy_config, self.cache_var, self.cache_var.get(), *self.cache_list_display
)
self.cache_list_dropdown.config(width=20)
self.cache_list_dropdown.grid(row=1, column=2, padx=5, pady=3)
self.add_context_help(self.cache_list_dropdown, CONTEXT_HELP['CACHE_DROPDOWN'])
# Load button (lambda function with boolean for cache)
ttk.Button(
label_frm_tenancy_config,
width=25,
text='Load from Tenancy',
command=lambda: self._on_load_clicked(use_cache=False),
).grid(row=0, column=3, padx=5, pady=5, sticky='w')
ttk.Button(
label_frm_tenancy_config,
width=25,
text='Load from Cache',
command=lambda: self._on_load_clicked(use_cache=True),
).grid(row=1, column=3, padx=5, pady=5, sticky='w')
def open_link(event):
link = 'https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/clitoken.htm'
logger.info(f'Opening link in browser: {link}')
webbrowser.open_new(link)
# Session Token (with link)
session_auth_link_label = ttk.Label(
label_frm_tenancy_config,
text='Session Token (oci session authenticate)',
cursor='hand2',
foreground='#0000EE', # Make it a link
)
self.add_context_help(
session_auth_link_label,
CONTEXT_HELP['SESSION_TOKEN'],
)
session_auth_link_label.bind('<Button-1>', open_link)
session_auth_link_label.grid(row=2, column=0, columnspan=2, padx=5, pady=3)
self.session_token_var = tk.StringVar()
self.session_token_entry = ttk.Entry(label_frm_tenancy_config, textvariable=self.session_token_var, width=25)
self.session_token_entry.grid(row=2, column=2, padx=5, pady=3)
ttk.Button(
label_frm_tenancy_config,
width=25,
text='Load via Session Token',
command=lambda: self._on_load_clicked(use_cache=False),
).grid(row=2, column=3, padx=5, pady=5, sticky='w')
ttk.Separator(label_frm_tenancy_config, orient=tk.VERTICAL).grid(
row=0, column=4, rowspan=4, padx=5, pady=5, sticky='w'
)
# Import / Export buttons
ttk.Button(
label_frm_tenancy_config,
width=30,
text='Import JSON (share/backup)',
command=lambda: self.app._import_cache_from_json(callback={'complete': self._on_load_finished}),
).grid(row=0, column=5, padx=5, pady=5, sticky='w')
ttk.Button(
label_frm_tenancy_config,
width=30,
text='Export JSON (share/backup)',
command=lambda: self.app._export_cache_to_json(),
).grid(row=1, column=5, padx=5, pady=5, sticky='w')
# --- New Button: Load from Compliance Output Data ---
def _on_load_compliance_output():
# from tkinter import filedialog
# import os
folder_selected = filedialog.askdirectory(title='Select Compliance Output Directory')
if not folder_selected or not os.path.isdir(folder_selected):
messagebox.showinfo('Load Compliance Output', 'No directory was selected or path is invalid.')
return
# Show quick UI progress
self.progress_var.set(f'Loading compliance data from: {folder_selected}')
# Call the app method (must exist/app-supports), use same callbacks as tenancy load
self.app.load_compliance_output_async(
folder_selected,
callback={
'progress': self._on_load_progress,
'complete': self._on_load_finished,
'error': self._on_load_finished,
},
load_all_users=self.load_all_users_var.get(),
)
self.btn_load_compliance = ttk.Button(
label_frm_tenancy_config,
width=30,
text='Load from Compliance Output Data',
command=_on_load_compliance_output,
)
self.btn_load_compliance.grid(row=2, column=5, padx=5, pady=5, sticky='w')
# --- Page Help context for Compliance Output button ---
def _show_compliance_help(_event=None):
self.set_page_help_text(CONTEXT_HELP['COMPLIANCE_OUTPUT'])
self.btn_load_compliance.bind('<Enter>', _show_compliance_help)
self.btn_load_compliance.bind('<Leave>', lambda e=None: self.set_page_help_text(self.default_help_text))
ttk.Separator(label_frm_tenancy_config, orient=tk.VERTICAL).grid(row=0, column=6, rowspan=4, pady=5, sticky='w')
# Progress indicator - move to its own row below buttons, at right
self.progress_var = tk.StringVar(value='')
self.progress_label = ttk.Label(label_frm_tenancy_config, textvariable=self.progress_var, foreground='blue')
self.progress_label.grid(row=1, column=6, padx=5, pady=(1, 5), sticky='w')
# Label Frame for AI Connection
self.label_frm_ai_config = ttk.Labelframe(self, text='OCI GenAI')
self.label_frm_ai_config.pack(fill='x', padx=5, pady=5)
# --- Page Help context for OCI GenAI ---
def _show_genai_help(_event=None):
self.set_page_help_text(CONTEXT_HELP['OCI_GENAI'])
self.label_frm_ai_config.bind('<Enter>', _show_genai_help)
self.label_frm_ai_config.bind('<Leave>', lambda e=None: self.set_page_help_text(self.default_help_text))
# AI Toggle
self.ai_toggle_btn = ttk.Button(
self.label_frm_ai_config, state=tk.DISABLED, text='(3) Toggle AI Pane', command=self.app.toggle_bottom
)
self.ai_toggle_btn.grid(row=0, column=0, padx=3, pady=3, sticky='ew')
def populate_model_tree():
"""Populate the model Treeview with available models from list_models."""
logger.info('Populating model tree')
# Check and initialize AI client if needed
if not self.ai_repo.initialized:
logger.info(
f'Creating AI Clients from selected Profile: {self.profile_var.get()} or IP:{self.ip_var.get()}.'
)
self.ai_repo.initialize_client(use_instance_principal=self.ip_var.get(), profile=self.profile_var.get())
# Grab endpoint/compartment for variable
self.endpoint_var.set(self.ai_repo.base_endpoint)
self.ai_compartment_var.set(self.ai_repo.tenancy_ocid)
logger.debug(f'AI initialized: {self.ai_repo.initialized}')
if self.ai_repo.initialized:
ai_models = self.ai_repo.list_models()
logger.info(f'list_models returned {len(ai_models)} models')
# Put in Model Data Table
self.ai_model_table.update_data(new_data=ai_models)
else:
logger.warning('AI client not initialized, cannot list models')
self.refresh_button = ttk.Button(
self.label_frm_ai_config, text='(1) Refresh Models (using selected profile)', command=populate_model_tree
)
self.refresh_button.grid(row=0, column=1, padx=3, pady=3, sticky='ew')
self.ai_progress_var = tk.StringVar(value='')
self.ai_progress_label = ttk.Label(
self.label_frm_ai_config, textvariable=self.ai_progress_var, foreground='blue'
)
self.ai_progress_label.grid(row=0, column=2, padx=3, pady=3, sticky='w')
def update_model_ocid(selected_items):
if selected_items:
model_ocid = selected_items[0].get('Model OCID', '')
self.model_id_var.set(model_ocid)
logger.info(f'Updated Model ID entry with OCID {model_ocid} from data table selection')
# Data Table for models
self.ai_model_table = DataTable(
parent=self.label_frm_ai_config,
columns=AI_MODEL_COLUMNS,
display_columns=AI_MODEL_COLUMNS,
column_widths=AI_MODEL_COLUMN_WIDTHS,
data=[],
selection_callback=update_model_ocid,
# row_context_menu_callback=model_ocid_right_click,
multi_select=False,
)
self.ai_model_table.grid(row=1, column=0, columnspan=3, padx=3, pady=3, sticky='ew')
self.model_id_var = tk.StringVar()
ttk.Label(self.label_frm_ai_config, text='Regional Endpoint:').grid(
row=3, column=0, padx=2, pady=3, sticky='ew'
)
self.endpoint_var = tk.StringVar()
self.endpoint_entry = ttk.Entry(self.label_frm_ai_config, textvariable=self.endpoint_var, width=80)
self.endpoint_entry.grid(row=3, column=1, padx=3, pady=3, sticky='ew')
ttk.Label(self.label_frm_ai_config, text='Compartment (for GenAI):').grid(
row=4, column=0, padx=2, pady=3, sticky='ew'
)
self.ai_compartment_var = tk.StringVar()
self.ai_compartment_entry = ttk.Entry(self.label_frm_ai_config, textvariable=self.ai_compartment_var, width=80)
self.ai_compartment_entry.grid(row=4, column=1, padx=3, pady=3, sticky='ew')
apply_button = ttk.Button(
self.label_frm_ai_config, text='(2) Apply and Test GenAI Settings', command=self.apply_config
)
apply_button.grid(row=2, column=2, rowspan=3, padx=3, pady=3, sticky='ew')
logger.debug('Apply button created')
# (Moved MCP block to top and made autosave; original section removed)
# -------------------------
# Context Help Handlers
# -------------------------
def _on_context_help_changed(self):
"""
Callback when Context Help checkbox is toggled.
Notifies main.py to globally propagate the update to all tabs using App.refresh_all_tabs_settings.
"""
self.settings['context_help'] = self.context_help_var.get()
config.save_settings(self.settings)
logger.info(f'Context Help setting changed to: {self.context_help_var.get()}')
if hasattr(self.app, 'refresh_all_tabs_settings'):
self.app.refresh_all_tabs_settings()
[docs]
def refresh_context_help(self):
"""Refresh style/visibility of Page Help label. (SettingsTab extension point)"""
BaseUITab.refresh_context_help(self) # type: ignore
# -------------------------
# Loading of tenancy buttons
# -------------------------
def _save_mcp_config(self):
"""Save full settings to disk."""
# Update MCP settings
try:
port_val = int(self.mcp_port_var.get())
except ValueError:
messagebox.showerror('Invalid Port', 'Port must be an integer.')
return
self.settings['mcp_port'] = port_val
self.settings['mcp_host'] = self.mcp_host_var.get().strip() or '127.0.0.1'
config.save_settings(self.settings)
logger.info('MCP configuration saved to settings.')
def _on_load_clicked(self, use_cache: bool):
"""Handle Load Tenancy button click. Calls main app to load tenancy asynchronously.
Args:
use_cache (bool): Whether to load from cache or live tenancy.
"""
self.settings['tenancy_ocid'] = self.tenancy_var.get()
self.settings['recursive'] = self.recursive_var.get()
self.settings['instance_principal'] = self.ip_var.get()
self.settings['named_profile'] = self.profile_var.get()
self.settings['ai_compartment_ocid'] = self.profile_var.get()
self.settings['load_all_users'] = self.load_all_users_var.get()
config.save_settings(self.settings)
self.app.load_tenancy_async(
tenancy_id=self.tenancy_var.get(),
recursive=self.recursive_var.get(),
instance_principal=self.ip_var.get(),
named_profile=self.profile_var.get() if not use_cache else None,
named_session=self.session_token_var.get() if self.session_token_var.get() != '' else None,
named_cache=self.display_to_cache_key.get(self.cache_var.get(), None) if use_cache else None,
load_all_users=self.load_all_users_var.get(),
callback={
'progress': self._on_load_progress,
'complete': self._on_load_finished,
'error': self._on_load_finished,
},
)
def _on_load_progress(self, message: str, clear: bool = False):
"""Callback from App to update progress during tenancy loading."""
self.progress_var.set(f'{message}')
if clear:
self.after(2000, lambda: self.progress_var.set(''))
def _on_load_finished(self, success: bool, message: str, clear: bool = False):
"""Callback from App once tenancy loading completes."""
if success:
# Format "data as of" date to "YYYY-Mon-DD hh:mi:ssZ"
data_as_of = getattr(getattr(self.app, 'policy_compartment_analysis', None), 'data_as_of', None)
if data_as_of:
import datetime
try:
dt = datetime.datetime.fromisoformat(data_as_of.replace('Z', '+00:00'))
date_str = dt.strftime('%Y-%b-%d %H:%M:%SZ')
except Exception:
date_str = str(data_as_of)
date_note = f'Data as of: {date_str}'
else:
date_note = ''
self.progress_var.set(f'[OK]{message}')
self.after(2000, lambda date_note=date_note: self.progress_var.set(date_note))
# logger.info('Updating UI after load')
# self.app.policies_tab.update_policy_output()
# self.app.policies_tab.enable_widgets_after_load()
# self.app.users_tab.update_user_analysis_output()
else:
self.progress_var.set(f'[X]{message}')
# Schedule it to go away if clear was set
if clear:
self.after(5000, lambda: self.progress_var.set(''))
# After loading, update the cache list in case new one was created
logger.info('Updating cache list after load')
self.refresh_cache_list()
[docs]
def refresh_cache_list(self):
"""Update the cache list OptionMenu in the Settings tab to reflect the current state."""
self.cache_list = self.caching.get_available_cache(None)
preserved_caches = self.caching.get_preserved_cache_set()
self.cache_list_display = [f'(P) {name}' if name in preserved_caches else name for name in self.cache_list]
self.display_to_cache_key = {
f'(P) {name}' if name in preserved_caches else name: name for name in self.cache_list
}
menu = self.cache_list_dropdown['menu']
menu.delete(0, 'end')
for display_name in self.cache_list_display:
menu.add_command(label=display_name, command=lambda value=display_name: self.cache_var.set(value))
if self.cache_list_display:
self.cache_var.set(self.cache_list_display[0])
else:
self.cache_var.set('No Cache Available')
def _on_load_all_users_changed(self):
"""Callback when Load All Users checkbox is toggled; saves to settings."""
self.settings['load_all_users'] = self.load_all_users_var.get()
config.save_settings(self.settings)
# Optionally: Trigger UI hide/show of user info on tabs if implemented
if hasattr(self.app, 'users_tab') and hasattr(self.app.users_tab, 'on_load_all_users_setting_changed'):
self.app.users_tab.on_load_all_users_setting_changed(self.load_all_users_var.get())
if hasattr(self.app, 'simulation_tab') and hasattr(
self.app.simulation_tab, 'on_load_all_users_setting_changed'
):
self.app.simulation_tab.on_load_all_users_setting_changed(self.load_all_users_var.get())
# -------------------------
# AI Enablement
# -------------------------
[docs]
def apply_config(self):
"""
Apply changes to Model ID and Endpoint in AI client.
"""
start_time = time.perf_counter()
model_id = self.model_id_var.get().strip()
endpoint = self.endpoint_var.get().strip()
compartment_ocid = self.ai_compartment_var.get().strip()
logger.info('Applying config changes: Model ID=%s, Endpoint=%s', model_id, endpoint)
self.ai_progress_var.set('[-] Running AI test call…')
try:
self.ai_repo.update_config(model_ocid=model_id, endpoint=endpoint, compartment_ocid=compartment_ocid)
logger.info(f'Configuration updated successfully in {time.perf_counter() - start_time:.2f} seconds')
# Make AI Call to test with callback
self.app.ask_genai_async(
prompt='What is the meaning of life?',
additional_instruction='TEST',
callback=self._on_ai_enablement_finished,
test_call=True, # Flag to indicate this is just a test call for enablement purposes
)
except Exception as e:
logger.error('Failed to update configuration: %s', e)
[docs]
def _on_ai_enablement_finished(self, success: bool, message: str, clear: bool = False): # noqa: C901
"""
Callback from App once AI loading completes. Used to enable AI toggle button if successful.
Args:
success (bool): Whether the AI call was successful.
message (str): Message to display.
clear (bool): Whether to clear the message after a delay.
"""
if success:
self.ai_progress_var.set(f'[OK] {message}')
# Enable the toggle button
self.ai_toggle_btn.config(state=tk.NORMAL)
# Enable AI Assist button on PoliciesTab after AI enablement success
if hasattr(self.app, 'policies_tab') and hasattr(self.app.policies_tab, 'ai_assist_btn'):
self.app.policies_tab.ai_assist_btn.config(state=tk.NORMAL)
logger.info('AI Assist button on PoliciesTab enabled')
# Enable AI Assist button on PolicyBrowserTab after AI enablement success
if hasattr(self.app, 'policy_browser_tab') and hasattr(self.app.policy_browser_tab, 'ai_assist_btn'):
self.app.policy_browser_tab.ai_assist_btn.config(state=tk.NORMAL)
logger.info('AI Assist button on PolicyBrowserTab enabled')
# Enable AI Assist button on DynamicGroupTab after AI enablement success
if hasattr(self.app, 'dynamic_groups_tab') and hasattr(self.app.dynamic_groups_tab, 'ai_assist_btn'):
self.app.dynamic_groups_tab.ai_assist_btn.config(state=tk.NORMAL)
logger.info('AI Assist button on DynamicGroupsTab enabled')
# Enable UsersTab (and other tabs in the future) to show the AI Assist button
if hasattr(self.app, 'users_tab') and hasattr(self.app.users_tab, 'ai_assist_btn'):
self.app.users_tab.ai_assist_btn.config(state=tk.NORMAL)
logger.info('AI Assist button on UsersTab enabled')
# Enable ResourcePrincipalsTab (and other tabs in the future) to show the AI Assist button
if hasattr(self.app, 'resource_principals_tab') and hasattr(
self.app.resource_principals_tab, 'ai_assist_btn'
):
self.app.resource_principals_tab.ai_assist_btn.config(state=tk.NORMAL)
logger.info('AI Assist button on ResourcePrincipalsTab enabled')
# Clear previous AI Assistant search and results after enablement
if hasattr(self.app, 'policy_query_var'):
self.app.policy_query_var.set('')
# if hasattr(self.app, "policy_query_label_text"):
# self.app.policy_query_label_text.set('')
self.app.output_text.delete('1.0', tk.END)
# Common for result/response variable: ai_output_var or genai_response_var or similar
if hasattr(self.app, 'ai_output_var'):
self.app.ai_output_var.set('')
if hasattr(self.app, 'genai_response_var'):
self.app.genai_response_var.set('')
else:
self.ai_progress_var.set(f'[X] {message}')
# Schedule it to go away if clear was set
if clear:
self.after(2000, lambda: self.ai_progress_var.set(''))
# -------------------------
# Console/ Debug Tab Toggle
# -------------------------
[docs]
def _toggle_console_tab(self):
"""Toggle the visibility of the console and debug tab in the notebook."""
notebook = self.app.notebook
console_tab = self.app.console_tab
debugger_tab = getattr(self.app, 'debugger_tab', None)
if self.app.console_visible:
notebook.forget(console_tab)
if debugger_tab:
notebook.forget(debugger_tab)
self.console_btn_var.set('Show Console and Debug Tab')
self.app.console_visible = False
logger.info('Console and Debug tabs hidden')
else:
notebook.add(console_tab, text='Console Logging\n(Internal)')
if debugger_tab:
notebook.add(debugger_tab, text='JSON Debugger\n(Internal)')
self.console_btn_var.set('Hide Console and Debug Tab')
self.app.console_visible = True
logger.info('Console and Debug tabs shown')
# -------------------------
# Maintenance Tab Toggle
# -------------------------
[docs]
def _toggle_maintenance_tab(self):
"""Toggle the visibility of the maintenance tab in the notebook."""
notebook = self.app.notebook
maintenance_tab = self.app.maintenance_tab
if self.app.maintenance_visible:
notebook.forget(maintenance_tab)
self.maintenance_btn_var.set('Show Maintenance Tab')
self.app.maintenance_visible = False
logger.info('Maintenance tab hidden')
else:
notebook.add(maintenance_tab, text='Maintenance\n(Internal)')
self.maintenance_btn_var.set('Hide Maintenance Tab')
self.app.maintenance_visible = True
logger.info('Maintenance tab shown')
# -------------------------
# Advanced Tabs Toggle
# -------------------------
def _toggle_advanced_tabs(self):
"""Toggle the visibility of the advanced tabs in the notebook."""
notebook = self.app.notebook
advanced_tabs = [
self.app.permissions_report_tab,
self.app.condition_tester_tab,
self.app.simulation_tab,
self.app.policy_recommendations_tab,
]
if self.app.advanced_tabs_visible:
for tab in advanced_tabs:
notebook.forget(tab)
self.advanced_btn_var.set('Show Advanced Tabs')
self.app.advanced_tabs_visible = False
logger.info('Advanced tabs hidden')
else:
notebook.add(self.app.permissions_report_tab, text='Permissions Report\n(Advanced)')
notebook.add(self.app.condition_tester_tab, text='Condition Tester\n(Advanced)')
notebook.add(self.app.simulation_tab, text='API Simulation\n(Advanced)')
notebook.add(self.app.policy_recommendations_tab, text='Policy Recommendations\n(Preview)')
self.advanced_btn_var.set('Hide Advanced Tabs')
self.app.advanced_tabs_visible = True
logger.info('Advanced tabs shown')