Source code for oci_policy_analysis.ui.maintenance_tab

##########################################################################
# 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/
#
# maintenance_tab.py
#
# @author: Andrew Gregory
#
# Supports Python 3.12 and above
#
# coding: utf-8
##########################################################################

import json
import tkinter as tk
from tkinter import messagebox, simpledialog, ttk
from tkinter.scrolledtext import ScrolledText

from oci_policy_analysis.common import config
from oci_policy_analysis.common.logger import get_logger

# Global logger for this module
logger = get_logger(component='maintenance_tab')


[docs] class MaintenanceTab(ttk.Frame): # Document with multi-line google-style napoleon comments for the class with public methods """ Maintenance Tab for OCI Policy Analysis UI. Provides cache management and permissions testing tools. Methods: __init__: Initializes the MaintenanceTab with UI components and callbacks. _refresh_maintenance_cache_list: (Internal) Refreshes the cache list display. _maintenance_remove_selected_cache: (Internal) Removes the selected cache entry. _maintenance_rename_selected_cache: (Internal) Renames the selected cache entry. _maintenance_preserve_selected_cache: (Internal) Toggles the preserve status of the selected cache entry. _maintenance_permissions_load_data: (Internal) Loads reference data for permissions testing. _maintenance_get_permission: (Internal) Retrieves permissions for the selected resource/family and verb. _maintenance_check_overlap: (Internal) Checks for overlap between two permission statements. """ def __init__(self, parent, app): super().__init__(parent) self.app = app self.caching = app.caching self.settings = app.settings self.settings_tab = app.settings_tab # Optional link for settings tab live refresh self.policies_tab = app.policies_tab # Optional link to policies tab # Build the UI components self.build_ui()
[docs] def build_ui(self): # -------- Maintenance UI Build (CACHE & SAVED SEARCHES, 50/50) ---------- frm_top = ttk.LabelFrame(self, text='Cache & Saved Search Management') frm_top.pack(fill='x', padx=10, pady=10) frm_top_content = ttk.Frame(frm_top) frm_top_content.pack(fill='x', expand=True) # --- LEFT: Caches --- frm_cache = ttk.Frame(frm_top_content) frm_cache.pack(side='left', fill='both', expand=True, padx=(0, 8)) cache_lbl = ttk.Label(frm_cache, text='Cache Entries', font=('TkDefaultFont', 10, 'bold')) cache_lbl.pack(anchor='nw', padx=3, pady=(2, 0)) # Listbox of caches self.maintenance_cache_list = tk.Listbox(frm_cache, selectmode=tk.SINGLE, height=8, width=40) self.maintenance_cache_list.pack(side='left', padx=8, pady=6, fill='y') self._refresh_maintenance_cache_list() # Do NOT call any reference-data dependent methods here; they are called by app when data is ready # Scrollbar for Listbox scroll = ttk.Scrollbar(frm_cache, orient='vertical', command=self.maintenance_cache_list.yview) self.maintenance_cache_list.config(yscrollcommand=scroll.set) scroll.pack(side='left', fill='y') # Buttons for cache ops btns_frm = ttk.Frame(frm_cache) btns_frm.pack(side='left', padx=5, fill='y') self.cache_remove_button = ttk.Button( btns_frm, text='Remove Selected', command=self._maintenance_remove_selected_cache ) self.cache_remove_button.pack(pady=2) self.cache_rename_button = ttk.Button( btns_frm, text='Rename Selected', command=self._maintenance_rename_selected_cache ) self.cache_rename_button.pack(pady=2) self.cache_preserve_button = ttk.Button( btns_frm, text='Toggle Preserve', command=self._maintenance_preserve_selected_cache ) self.cache_preserve_button.pack(pady=2) # Feedback/status self.maintenance_status_var = tk.StringVar(value='') self.maintenance_status_label = ttk.Label( frm_cache, textvariable=self.maintenance_status_var, foreground='blue' ) self.maintenance_status_label.pack(side='bottom', fill='x', pady=(4, 0)) # --- RIGHT: Saved Searches --- frm_saved = ttk.Frame(frm_top_content) frm_saved.pack(side='left', fill='both', expand=True, padx=(8, 0)) saved_lbl = ttk.Label(frm_saved, text='Saved Searches', font=('TkDefaultFont', 10, 'bold')) saved_lbl.pack(anchor='nw', padx=3, pady=(2, 0)) self.saved_search_list = tk.Listbox(frm_saved, selectmode=tk.SINGLE, height=8, width=40) self.saved_search_list.pack(side='left', padx=8, pady=6, fill='y') # Example data for saved searches -- replace with real integration as needed self._refresh_saved_search_list() # Do NOT call any reference-data dependent methods here; they are called by app when data is ready saved_scroll = ttk.Scrollbar(frm_saved, orient='vertical', command=self.saved_search_list.yview) self.saved_search_list.config(yscrollcommand=saved_scroll.set) saved_scroll.pack(side='left', fill='y') saved_btns_frm = ttk.Frame(frm_saved) saved_btns_frm.pack(side='left', padx=5, fill='y') self.saved_remove_button = ttk.Button( saved_btns_frm, text='Delete Selected', command=self._maintenance_remove_selected_search ) self.saved_remove_button.pack(pady=2) self.saved_rename_button = ttk.Button( saved_btns_frm, text='Rename Selected', command=self._maintenance_rename_selected_search ) self.saved_rename_button.pack(pady=2) # No preservation button for saved searches # Feedback label for saved search actions self.saved_search_status_var = tk.StringVar(value='') self.saved_search_status_label = ttk.Label( frm_saved, textvariable=self.saved_search_status_var, foreground='blue' ) self.saved_search_status_label.pack(side='bottom', fill='x', pady=(4, 0)) # -------- Maintenance UI Build (PERMISSIONS/OPERATIONS JSON VIEWERS) ---------- frm_json_wrap = ttk.Frame(self) frm_json_wrap.pack(fill='x', padx=10, pady=(0, 5)) # Permissions JSON (Raw/Debug) frm_json = ttk.LabelFrame(frm_json_wrap, text='Permissions JSON (Raw/Debug)') frm_json.pack(side='left', fill='both', expand=True, padx=(0, 4)) self.json_text = ScrolledText(frm_json, height=10, width=70, wrap=tk.WORD) self.json_text.pack(pady=5, fill='both', expand=True) # self._maintenance_display_json() # (Commented: now done in refresh_data) # Removed Save JSON button # Operations JSON (Raw/Debug) to the right frm_ops_json = ttk.LabelFrame(frm_json_wrap, text='Operations JSON (Raw/Debug)') frm_ops_json.pack(side='left', fill='both', expand=True, padx=(4, 0)) self.operations_json_text = ScrolledText(frm_ops_json, height=10, width=70, wrap=tk.WORD) self.operations_json_text.pack(pady=5, fill='both', expand=True) # self._maintenance_display_operations_json() # (Commented: now done in refresh_data) # -------- Main Maintenance UI Build (PERMISSIONS TESTER and API OPERATIONS TESTER side-by-side) ---------- frm_testers_wrap = ttk.Frame(self) frm_testers_wrap.pack(fill='both', padx=10, pady=(0, 10), expand=True) # Permissions Viewer & Tests (LEFT) frm_permissions = ttk.LabelFrame(frm_testers_wrap, text='Permissions Viewer & Tests') frm_permissions.pack(side='left', fill='both', expand=True, padx=(0, 8)) row0 = ttk.Frame(frm_permissions) row0.pack(fill='x', pady=2) ttk.Label(row0, text='Resource/Family:').pack(side='left', padx=2) self.permissions_resource_combo = ttk.Combobox(row0, width=30) self.permissions_resource_combo.pack(side='left') ttk.Label(row0, text='Verb:').pack(side='left', padx=2) self.permissions_verb_combo = ttk.Combobox(row0, values=['inspect', 'read', 'use', 'manage'], width=10) self.permissions_verb_combo.pack(side='left') ttk.Button(row0, text='Get Permissions', command=self._maintenance_get_permission).pack(side='left', padx=4) self.permissions_result_label = ttk.Label(frm_permissions, text='', wraplength=300, justify='left') self.permissions_result_label.pack(fill='x', pady=2, padx=2) sep = ttk.Separator(frm_permissions, orient='horizontal') sep.pack(fill='x', pady=4) overlap_stmt1 = ttk.Frame(frm_permissions) overlap_stmt1.pack(fill='x', pady=1) ttk.Label(overlap_stmt1, text='Stmt 1:').pack(side='left') self.permissions_res1_combo = ttk.Combobox(overlap_stmt1, width=20) self.permissions_res1_combo.pack(side='left') ttk.Label(overlap_stmt1, text='Verb:').pack(side='left') self.permissions_verb1_combo = ttk.Combobox( overlap_stmt1, values=['inspect', 'read', 'use', 'manage'], width=10 ) self.permissions_verb1_combo.pack(side='left') ttk.Label(overlap_stmt1, text='Action:').pack(side='left') self.permissions_action1_combo = ttk.Combobox(overlap_stmt1, values=['allow', 'deny'], width=8) self.permissions_action1_combo.set('allow') self.permissions_action1_combo.pack(side='left') overlap_stmt2 = ttk.Frame(frm_permissions) overlap_stmt2.pack(fill='x', pady=1) ttk.Label(overlap_stmt2, text='Stmt 2:').pack(side='left') self.permissions_res2_combo = ttk.Combobox(overlap_stmt2, width=20) self.permissions_res2_combo.pack(side='left') ttk.Label(overlap_stmt2, text='Verb:').pack(side='left') self.permissions_verb2_combo = ttk.Combobox( overlap_stmt2, values=['inspect', 'read', 'use', 'manage'], width=10 ) self.permissions_verb2_combo.pack(side='left') ttk.Label(overlap_stmt2, text='Action:').pack(side='left') self.permissions_action2_combo = ttk.Combobox(overlap_stmt2, values=['allow', 'deny'], width=8) self.permissions_action2_combo.set('allow') self.permissions_action2_combo.pack(side='left') ttk.Button(overlap_stmt2, text='Check Overlap', command=self._maintenance_check_overlap).pack( side='left', padx=12 ) self.permissions_overlap_text = tk.Text(frm_permissions, height=14, width=50, wrap=tk.WORD) self.permissions_overlap_text.pack(fill='both', padx=2, pady=2, expand=True) # self._maintenance_permissions_load_data() # (Commented: now done in refresh_data) # API Operations Tester (RIGHT) frm_ops_tester_lbl = ttk.LabelFrame(frm_testers_wrap, text='API Operations Tester') frm_ops_tester_lbl.pack(side='left', fill='both', expand=True, padx=(8, 0), ipadx=5) # --- API Operation Search/Filter --- ttk.Label(frm_ops_tester_lbl, text='API Operation:\n(Search and Select)').grid( row=0, column=0, sticky='w', padx=3, pady=2 ) self.apiop_filter_var = tk.StringVar() self.apiop_filter_entry = ttk.Entry(frm_ops_tester_lbl, textvariable=self.apiop_filter_var, width=42) self.apiop_filter_entry.grid(row=0, column=1, sticky='we', padx=3, pady=(2, 0)) self.apiop_filter_entry.bind('<KeyRelease>', self._on_apiop_filter_change) self.apiop_combo = ttk.Combobox(frm_ops_tester_lbl, state='readonly', width=44) self.apiop_combo.grid(row=1, column=1, sticky='w', padx=3, pady=2) ttk.Label(frm_ops_tester_lbl, text='All Known Permissions:\n(Select multiple with Ctrl)').grid( row=2, column=0, sticky='nw', padx=3 ) self.apiop_perms_listbox = tk.Listbox(frm_ops_tester_lbl, selectmode='multiple', height=9, width=42) self.apiop_perms_listbox.grid(row=2, column=1, sticky='w', padx=3, pady=2) self.apiop_perms_listbox.bind('<<ListboxSelect>>', self._on_apiop_perm_select) # Permissions Selected (Preview) ttk.Label(frm_ops_tester_lbl, text='Selected Permissions:').grid(row=3, column=0, sticky='nw', padx=3) self.apiop_selectedperms_text = ScrolledText( frm_ops_tester_lbl, height=5, width=42, wrap=tk.WORD, state=tk.DISABLED, font=('Consolas', 10) ) self.apiop_selectedperms_text.grid(row=3, column=1, padx=3, pady=(1, 3), sticky='we') self.apiop_check_btn = ttk.Button(frm_ops_tester_lbl, text='Check API', command=self._apiop_check) self.apiop_check_btn.grid(row=4, column=1, sticky='w', pady=(2, 2), padx=3) self.apiop_result_label = ttk.Label(frm_ops_tester_lbl, text='', width=20, font=('Consolas', 11, 'bold')) self.apiop_result_label.grid(row=4, column=0, sticky='e', padx=3) # Note label for operation-specific notes self.apiop_note_label = ttk.Label( frm_ops_tester_lbl, text='', wraplength=350, font=('TkDefaultFont', 9, 'italic'), foreground='slate gray', justify='left', ) self.apiop_note_label.grid(row=5, column=0, columnspan=2, sticky='w', padx=3, pady=(6, 3))
# self._maintenance_ops_tester_load_data() # (Commented: now done in refresh_data)
[docs] def refresh_data(self): """ Public method: must be called after reference data is loaded and ready. Loads or reloads all data-dependent UI elements (comboboxes, JSON viewers, permissions, etc). """ logger.info( f'Refreshing MaintenanceTab data and UI elements...{self.app.reference_data_repo.data.keys() if hasattr(self.app, "reference_data_repo") else "No repo available"}' ) self._ref_repo = self.app.reference_data_repo self._maintenance_permissions_load_data() self._maintenance_display_json() self._maintenance_display_operations_json() self._maintenance_ops_tester_load_data() self._refresh_saved_search_list() logger.info( f'MaintenanceTab data refresh - Saved searches count: {len(self.settings.get("saved_policy_searches", []))}.' )
def _refresh_saved_search_list(self): """Populate the saved searches Listbox using app settings (shared with policies tab).""" if 'saved_policy_searches' not in self.settings or not isinstance(self.settings['saved_policy_searches'], list): self.settings['saved_policy_searches'] = [] self.saved_search_list.delete(0, tk.END) for search in self.settings['saved_policy_searches']: name = search.get('name', '') self.saved_search_list.insert(tk.END, name) def _maintenance_remove_selected_search(self): idx = self.saved_search_list.curselection() if not idx: self.saved_search_status_var.set('Select a saved search to delete.') return search_name = self.saved_search_list.get(idx[0]) # Remove from settings found_index = next( (i for i, s in enumerate(self.settings['saved_policy_searches']) if s.get('name', '') == search_name), None ) if found_index is not None: del self.settings['saved_policy_searches'][found_index] config.save_settings(self.settings) self._refresh_saved_search_list() self.saved_search_status_var.set(f'Removed "{search_name}"') # --- Update PoliciesTab saved search dropdown if present --- try: if getattr(self, 'app', None) and hasattr(self.app, 'policies_tab'): self.app.policies_tab._refresh_saved_searches_dropdown() except Exception: pass else: self.saved_search_status_var.set('Name not found.') def _maintenance_rename_selected_search(self): idx = self.saved_search_list.curselection() if not idx: self.saved_search_status_var.set('Select a saved search to rename.') return search_name = self.saved_search_list.get(idx[0]) new_name = simpledialog.askstring('Rename Saved Search', 'Enter new name:', initialvalue=search_name) if not new_name or new_name == search_name: self.saved_search_status_var.set('Rename cancelled or no change.') return # Check for duplicate name names = [s.get('name', '') for s in self.settings['saved_policy_searches']] if new_name in names: self.saved_search_status_var.set('Name already exists.') return # Find and update found = next((s for s in self.settings['saved_policy_searches'] if s.get('name', '') == search_name), None) if found: found['name'] = new_name config.save_settings(self.settings) self._refresh_saved_search_list() self.saved_search_status_var.set(f'Renamed to "{new_name}"') else: self.saved_search_status_var.set('Name not found.') def _maintenance_display_json(self): """Refresh the raw permissions JSON in the debug display.""" if hasattr(self, '_ref_repo'): json_str = json.dumps(self._ref_repo.data, indent=2) logger.debug(f'Permissions JSON data keys: {self._ref_repo}') else: try: # lazy-load repo if not loaded json_str = json.dumps(self._ref_repo.data, indent=2) except Exception as ex: json_str = f'Error loading permissions data: {ex}' self.json_text.delete(1.0, tk.END) self.json_text.insert(tk.END, json_str) def _maintenance_display_operations_json(self): """Show operations_by_api JSON (API + Operations) in side panel.""" import json if hasattr(self, '_ref_repo'): display_data = self._ref_repo.data.get('operations_by_api', {}) json_str = json.dumps(display_data, indent=2) else: try: json_str = json.dumps(self._ref_repo.data.get('operations_by_api', {}), indent=2) except Exception as ex: json_str = f'Error loading operations data: {ex}' self.operations_json_text.delete(1.0, tk.END) self.operations_json_text.insert(tk.END, json_str) def _maintenance_save_json(self): """Save not currently supported for reference data.""" messagebox.showinfo('Not supported', 'Save operation is not supported for reference data in this UI.')
[docs] def _refresh_maintenance_cache_list(self): """Populate the cache Listbox with available caches and their preserved status.""" caches = self.caching.get_available_cache(None) self.maintenance_cache_list.delete(0, tk.END) # To show preserved/non-preserved, read cache_entries.json import json import os entries_path = os.path.join(str(self.caching.cache_dir), 'cache_entries.json') preserved_map = {} try: with open(entries_path, encoding='utf-8') as f: for line in f: try: entry = json.loads(line) key = f"{entry['tenancy_name']}_{entry['cache_date']}" preserved_map[key] = entry.get('preserved', False) except Exception: continue except Exception: pass for cache in caches: preserved = preserved_map.get(cache, False) entry_str = f'(P) {cache}' if preserved else f'{cache}' self.maintenance_cache_list.insert(tk.END, entry_str)
[docs] def _maintenance_remove_selected_cache(self): idx = self.maintenance_cache_list.curselection() if not idx: self.maintenance_status_var.set('Select cache to remove.') return entry_str = self.maintenance_cache_list.get(idx[0]) cache_name = entry_str.replace('(P) ', '') # Remove prefix if present if self.caching.remove_cache_entry(cache_name): self.maintenance_status_var.set(f'Removed {cache_name}') self._refresh_maintenance_cache_list() else: self.maintenance_status_var.set(f'Failed to remove {cache_name}')
[docs] def _maintenance_rename_selected_cache(self): idx = self.maintenance_cache_list.curselection() if not idx: self.maintenance_status_var.set('Select cache to rename.') return entry_str = self.maintenance_cache_list.get(idx[0]) cache_name = entry_str.replace('(P) ', '') new_name = simpledialog.askstring( 'Rename Cache', 'Enter new name (format tenancy_cache-date):', initialvalue=cache_name ) if new_name and new_name != cache_name: if self.caching.rename_cache_entry(cache_name, new_name): self.maintenance_status_var.set(f'Renamed {cache_name} to {new_name}') self._refresh_maintenance_cache_list() # Refresh the cache list in settings tab as well (live update) if getattr(self, 'settings_tab', None): try: self.settings_tab.refresh_cache_list() except Exception: pass else: self.maintenance_status_var.set(f'Failed to rename {cache_name}')
[docs] def _maintenance_preserve_selected_cache(self): idx = self.maintenance_cache_list.curselection() if not idx: self.maintenance_status_var.set('Select cache to preserve/unpreserve.') return entry_str = self.maintenance_cache_list.get(idx[0]) cache_name = entry_str.replace('(P) ', '') is_preserved = entry_str.startswith('(P) ') if self.caching.preserve_cache_entry(cache_name, preserve=not is_preserved): if not is_preserved: self.maintenance_status_var.set(f'Marked {cache_name} as preserved') else: self.maintenance_status_var.set(f'Unmarked {cache_name} as preserved') self._refresh_maintenance_cache_list() # Live update settings tab cache list if present if getattr(self, 'settings_tab', None): try: self.settings_tab.refresh_cache_list() except Exception: pass else: self.maintenance_status_var.set(f'Failed to update preserve state for {cache_name}') logger.info(f'Cache "{cache_name}" preserve toggled to {not is_preserved}.')
[docs] def _maintenance_permissions_load_data(self): # Load reference data try: all_items = sorted(self._ref_repo.data['resources'].keys()) + [ f'Family: {f}' for f in sorted(self._ref_repo.data['families'].keys()) ] for widget_combo in [ self.permissions_resource_combo, self.permissions_res1_combo, self.permissions_res2_combo, ]: widget_combo['values'] = all_items if all_items: widget_combo.current(0) for vcombo in [self.permissions_verb_combo, self.permissions_verb1_combo, self.permissions_verb2_combo]: vcombo.set('read') for acombo in [self.permissions_action1_combo, self.permissions_action2_combo]: acombo.set('allow') except Exception as ex: self.permissions_result_label.config(text=f'Could not load permissions.json: {ex}')
[docs] def _maintenance_get_permission(self): sel = self.permissions_resource_combo.get() verb = self.permissions_verb_combo.get() self.permissions_result_label.config(text='') if sel and verb and hasattr(self, '_ref_repo'): is_family = sel.startswith('Family: ') entity = sel.replace('Family: ', '') if is_family else sel perms = self._ref_repo.get_permissions(entity, verb) label = f'Family: {entity}' if is_family else entity if perms is None: self.permissions_result_label.config(text='Invalid selection.') else: source = self._ref_repo.get_source(entity) source_text = f'\nSource URL: {source}' if source else '' if perms: upper_perms = [p.upper() for p in perms] self.permissions_result_label.config( text=f"{label} | {verb}: {', '.join(upper_perms)}{source_text}" ) else: self.permissions_result_label.config(text=f'{label} | {verb}: (no permissions){source_text}') else: self.permissions_result_label.config(text='Select resource/family and verb.')
def _on_apiop_perm_select(self, event=None): """Display selected permissions in the preview text box (non-editable).""" sel_indices = self.apiop_perms_listbox.curselection() perms = [self._apiop_perms[i] for i in sel_indices] display = ', '.join(perms) self.apiop_selectedperms_text.config(state=tk.NORMAL) self.apiop_selectedperms_text.delete(1.0, tk.END) self.apiop_selectedperms_text.insert(tk.END, display) self.apiop_selectedperms_text.config(state=tk.DISABLED) # No return (used as callback) def _maintenance_ops_tester_load_data(self): """Populate the operations tester controls with available APIs and permissions.""" # API operations as "apiname:Operation" apiops = [] op_to_api = {} for api, ops in self._ref_repo.data.get('operations_by_api', {}).items(): for op in ops: label = f'{api}:{op}' apiops.append(label) op_to_api[label] = (api, op) self._apiop_all_labels = apiops # save unfiltered for search/filter self._apiop_map = op_to_api # cache for callback self._update_apiop_combo_filtered() # Permissions: union of all permissions in all resources/verbs permset = set() for resval in self._ref_repo.data.get('resources', {}).values(): verbs = resval.get('verbs', {}) for plist in verbs.values(): permset.update(p.upper() for p in plist) perms = sorted(permset) self.apiop_perms_listbox.delete(0, tk.END) for p in perms: self.apiop_perms_listbox.insert(tk.END, p) self._apiop_perms = perms # for quick lookup # Deselect all, clear selected preview box self.apiop_perms_listbox.selection_clear(0, tk.END) self._on_apiop_perm_select() self._update_apiop_note() # Bind combobox change to update note as well self.apiop_combo.bind('<<ComboboxSelected>>', self._on_apiop_combo_change) def _update_apiop_combo_filtered(self, event=None): """Update API operation combobox values based on current filter entry (case-insensitive substring match).""" filter_txt = self.apiop_filter_var.get().strip().lower() if not filter_txt: filtered = self._apiop_all_labels else: filtered = [label for label in self._apiop_all_labels if filter_txt in label.lower()] self.apiop_combo['values'] = filtered if filtered: self.apiop_combo.current(0) else: self.apiop_combo.set('') def _on_apiop_filter_change(self, event=None): self._update_apiop_combo_filtered() self._on_apiop_combo_change() def _apiop_check(self): """Check if selected permissions satisfy the selected API op.""" if not hasattr(self, '_ref_repo'): messagebox.showerror('Error', 'Reference data not loaded') return op_label = self.apiop_combo.get() if not op_label or op_label not in self._apiop_map: self.apiop_result_label.config(text='(no op)') self.apiop_note_label.config(text='') return api_name, op_name = self._apiop_map[op_label] # Gather selected permissions from the listbox sel_indices = self.apiop_perms_listbox.curselection() perms = [self._apiop_perms[i] for i in sel_indices] res = self._ref_repo.has_api_operation_permissions(op_name, perms) self.apiop_result_label.config(text='True' if res else 'False', foreground='green' if res else 'red') # Also update selected permissions view in case selection changed via keyboard while focus on button self._on_apiop_perm_select() self._update_apiop_note() def _on_apiop_combo_change(self, event=None): """Update note field when changing operation combobox selection.""" self._update_apiop_note() def _update_apiop_note(self): # Show the note for the selected operation (if present) op_label = self.apiop_combo.get() note = '' if hasattr(self, '_ref_repo') and hasattr(self, '_apiop_map') and op_label in self._apiop_map: api_name, op_name = self._apiop_map[op_label] opmeta = self._ref_repo.data.get('operations_by_api', {}).get(api_name, {}).get(op_name, {}) note = opmeta.get('notes', '') or '' self.apiop_note_label.config(text=note if note else '')
[docs] def _maintenance_check_overlap(self): sel1 = self.permissions_res1_combo.get() verb1 = self.permissions_verb1_combo.get() action1 = self.permissions_action1_combo.get() sel2 = self.permissions_res2_combo.get() verb2 = self.permissions_verb2_combo.get() action2 = self.permissions_action2_combo.get() self.permissions_overlap_text.delete(1.0, tk.END) logger.info( f'User action: Checking overlap between {sel1} ({verb1}, {action1}) and {sel2} ({verb2}, {action2})' ) if not (sel1 and verb1 and action1 and sel2 and verb2 and action2 and hasattr(self, '_ref_repo')): logger.debug('Overlap: selection incomplete.') self.permissions_overlap_text.insert(tk.END, 'Select both statements.') return entity1 = sel1.replace('Family: ', '') if sel1.strip().startswith('Family:') else sel1 entity2 = sel2.replace('Family: ', '') if sel2.strip().startswith('Family:') else sel2 overlap = [] try: # Direct API with logging support logger.debug( f'Calling ReferenceDataRepo.check_overlap_params({entity1}, {verb1}, {action1}, {entity2}, {verb2}, {action2})' ) overlap = self._ref_repo.check_overlap_params(entity1, verb1, action1, entity2, verb2, action2) except Exception as ex: logger.error(f'Error in overlap check: {ex}') self.permissions_overlap_text.insert(tk.END, f'Error: {ex}') return output = f'Overlap between:\n {sel1} | {verb1} | {action1}\n {sel2} | {verb2} | {action2}\n\n' output += f"Common permissions: {', '.join(sorted(p.upper() for p in overlap)) if overlap else '(none)'}" logger.info(f'Overlap result = {output}') # Display result self.permissions_overlap_text.insert(tk.END, output)