"""Combined overview, load settings, and collapsible logging controls."""
import configparser
from pathlib import Path
from PySide6.QtCore import QSignalBlocker, Qt, QTimer, Signal
from PySide6.QtWidgets import (
QBoxLayout,
QCheckBox,
QComboBox,
QFileDialog,
QFrame,
QGridLayout,
QHBoxLayout,
QLabel,
QLayout,
QPlainTextEdit,
QPushButton,
QScrollArea,
QSpinBox,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
from oci_policy_analysis.application.context import AppContext
from oci_policy_analysis.application.core.support.caching import CacheManager
from oci_policy_analysis.application.services.cache_service import CacheService
from oci_policy_analysis.application.services.load_service import LoadResult
from oci_policy_analysis.presentation.qt.preferences import save_preferences
from oci_policy_analysis.presentation.qt.profiles import profile_names
from oci_policy_analysis.presentation.qt.settings_panels import AppearancePanel, IntelligencePanel
from oci_policy_analysis.presentation.qt.widgets import CollapsibleSection, SegmentedControl, linked_label
from oci_policy_analysis.presentation.qt.widgets import label as _label
[docs]
class OverviewSettingsPage(QScrollArea):
"""Own loading inputs and snapshot overview; emit requests to the controller."""
load_requested = Signal(str, dict, str)
browse_requested = Signal()
show_log_requested = Signal()
usage_changed = Signal(bool)
def __init__(self, logging_panel: QWidget, settings: dict, experience=None) -> None:
"""Restore the selected load source and start with settings sections collapsed."""
super().__init__()
self.settings = settings
# Load lifecycle flags keep controls and status text in sync with the
# worker and embedded MCP lock without replacing the active snapshot.
self._responsive_rows = []
self._busy = False
self._load_locked = False
self._load_preferences_dirty = False
self._loaded_connection_signature = None
self._loaded_status = 'Not loaded'
self.preference_timer = QTimer(self)
self.preference_timer.setSingleShot(True)
self.preference_timer.setInterval(300)
self.preference_timer.timeout.connect(self.save_load_preferences)
self.cache_service = CacheService(CacheManager())
self.setWidgetResizable(True)
self.setFrameShape(QFrame.Shape.NoFrame)
content = QWidget()
content.setObjectName('settingsContent')
layout = QVBoxLayout(content)
layout.setSizeConstraint(QLayout.SizeConstraint.SetMinAndMaxSize)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
if experience is not None:
mode = QFrame()
mode.setObjectName('card')
controls = QVBoxLayout(mode)
self.advanced_mode = QCheckBox('Advanced mode')
self.advanced_mode.setChecked(experience.advanced)
self.advanced_mode.setAccessibleDescription(
'Show MCP & assistant, prospective statements, simulation, tag-based policies, consolidation and advanced tools.'
)
self.advanced_mode.toggled.connect(experience.set_advanced)
experience.changed.connect(self.advanced_mode.setChecked)
controls.addWidget(self.advanced_mode)
controls.addWidget(
_label(
'Standard mode keeps everyday browsing and recommendations easy to find. Enable advanced mode for MCP & assistant, what-if policy editing, simulation, tag-based policies, consolidation and maintenance. You can switch back without reloading data.',
'muted',
wrap=True,
)
)
self.mode_status = _label('', 'muted')
experience.status.connect(self.mode_status.setText)
controls.addWidget(self.mode_status)
layout.addWidget(mode)
layout.addWidget(self._load_controls())
if experience is not None:
experience.bind_advanced(self.policy_loading_controls)
experience.bind_advanced(self.recursive)
experience.bind_advanced(self.load_all_users)
else:
self.policy_loading_controls.hide()
self.recursive.hide()
self.load_all_users.hide()
for signal in (
self.profile.currentTextChanged,
self.auth.currentIndexChanged,
self.recursive.toggled,
self.load_all_users.toggled,
self.depth.valueChanged,
self.policy_load_method.currentIndexChanged,
self.source_selector.currentIndexChanged,
):
signal.connect(self._load_preferences_changed)
# Snapshot details live under a persistent status header so load state
# remains visible even before or between successful loads.
self.active_data_card = self._active_data_card()
layout.addWidget(self.active_data_card)
self.results = self._overview_page()
self.results.hide()
self.active_data_layout.addWidget(self.results)
layout.addWidget(self._separator())
layout.addWidget(_label('Additional Settings', 'eyebrow'))
self.logging_panel = logging_panel
self.intelligence_panel = IntelligencePanel(settings)
self.appearance_panel = AppearancePanel(settings)
self.sections = {}
for name, panel in [
('Logging', logging_panel),
('Intelligence', self.intelligence_panel),
('Look and Feel', self.appearance_panel),
]:
section = CollapsibleSection(name, panel)
self.sections[name] = section
layout.addWidget(section)
logging_panel.show_log_requested.connect(self.show_log_requested.emit)
privacy = QWidget()
privacy_layout = QVBoxLayout(privacy)
self.usage_tracking = QCheckBox('Publish anonymous usage data')
self.usage_tracking.setChecked(settings.get('usage_tracking_enabled', True))
privacy_layout.addWidget(self.usage_tracking)
self.usage_notice = _label(
'Enabled by default. Publishes app version, OS/Python version, timestamps, feature usage, '
'inventory counts and the last six tenancy OCID characters to the project usage bucket. '
'No policy text, names, full OCIDs, search text, prompts or credentials are included. '
'Best-effort uploads occur on exit or tenancy change. Turn off to discard pending usage data.',
wrap=True,
)
privacy_layout.addWidget(self.usage_notice)
self.usage_tracking.toggled.connect(self.usage_changed.emit)
self.sections['Usage tracking'] = CollapsibleSection('Usage tracking', privacy)
layout.addWidget(self.sections['Usage tracking'])
layout.addStretch()
self.setWidget(content)
def _load_controls(self) -> QWidget:
"""Build the source switch and controls for each supported input type."""
card = QFrame()
card.setObjectName('card')
layout = QVBoxLayout(card)
layout.setContentsMargins(16, 14, 16, 14)
layout.addWidget(_label('Load data', 'eyebrow'))
# One selector swaps the four source-specific panels below it.
self.source_keys = ('oci', 'snapshot', 'compliance', 'sample')
self.source_selector = SegmentedControl(['Live Tenancy', 'Local Snapshot', 'CIS Import', 'Sample'])
layout.addWidget(self.source_selector)
self.source_panels = [QWidget() for _ in self.source_keys]
for panel in self.source_panels:
layout.addWidget(panel)
live = QVBoxLayout(self.source_panels[0])
live.setContentsMargins(0, 8, 0, 0)
grid = QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(10)
grid.setVerticalSpacing(8)
live.addLayout(grid)
# Authentication controls occupy the first row; the six-column grid
# keeps both selectors aligned when the page is resized.
self.auth = QComboBox()
for title, mode in [
('OCI config profile', 'profile'),
('Session token profile', 'session_token'),
('Instance principal', 'instance_principal'),
]:
self.auth.addItem(title, mode)
self.profile = QComboBox()
self.profile.setEditable(True)
try:
self.profile.addItems(profile_names())
except (OSError, configparser.Error) as exc:
self.profile.addItem('DEFAULT')
self.profile.setToolTip(f'Could not read OCI profiles: {exc}. Enter a profile name manually.')
self.profile.setCurrentText(str(self.settings.get('named_profile') or 'DEFAULT'))
mode = self.settings.get('qt_auth_mode')
if self.settings.get('instance_principal', False):
mode = 'instance_principal'
elif mode not in {'profile', 'session_token'}:
mode = 'profile'
self.auth.setCurrentIndex(self.auth.findData(mode))
self.profile.setEnabled(mode in {'profile', 'session_token'})
self.auth.currentIndexChanged.connect(
lambda _index: self.profile.setEnabled(self.auth.currentData() in {'profile', 'session_token'})
)
self.profile.setMinimumWidth(160)
grid.addWidget(_label('Authentication'), 0, 0)
grid.addWidget(self.auth, 0, 1, 1, 2)
grid.addWidget(_label('Profile'), 0, 3)
profile_row = QHBoxLayout()
profile_row.addWidget(self.profile, 1)
self.reload_profiles_button = QToolButton()
self.reload_profiles_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
self.reload_profiles_button.setAccessibleName('Reload OCI profiles')
self.reload_profiles_button.setToolTip(
'Reload profiles from ~/.oci/config. After session authentication creates a profile, '
'reload to see it in this list.'
)
self.reload_profiles_button.clicked.connect(self._reload_profiles)
profile_row.addWidget(self.reload_profiles_button)
grid.addLayout(profile_row, 0, 4, 1, 2)
self.auth_help = linked_label(
'For session authentication, choose the config profile that contains the session token.',
'Usage guide',
'https://agregory999.github.io/oci-policy-analysis/usage.html#overview',
wrap=True,
)
grid.addWidget(self.auth_help, 3, 0, 1, 6)
grid.setColumnStretch(1, 1)
grid.setColumnStretch(2, 1)
grid.setColumnStretch(4, 1)
grid.setColumnStretch(5, 1)
# Scope controls share the second row. The two checkboxes are advanced
# options, while domain depth remains available in Standard mode.
self.recursive = QCheckBox('All compartments')
self.recursive.setChecked(bool(self.settings.get('recursive', True)))
self.recursive.setToolTip('Include the tenancy root and all child compartments.')
self.load_all_users = QCheckBox('Load all users')
self.load_all_users.setChecked(bool(self.settings.get('load_all_users', True)))
self.depth = QSpinBox()
self.depth.setRange(1, 6)
try:
depth = int(self.settings.get('domain_compartment_depth', 1))
except (TypeError, ValueError):
depth = 1
self.depth.setValue(max(1, min(6, depth)))
self.depth.setFixedWidth(85)
self.depth.setToolTip('1 = root; 2 = root and immediate children. Independent of policy recursion.')
grid.addWidget(_label('Compartment Depth - Domain Search'), 1, 0, 1, 2)
grid.addWidget(self.depth, 1, 2)
grid.addWidget(self.load_all_users, 1, 3)
grid.addWidget(self.recursive, 1, 4, 1, 2)
# The final row pairs the primary action at the left with advanced
# policy-fetch behavior at the right.
self.policy_loading_controls = QWidget()
policy_loading = QHBoxLayout(self.policy_loading_controls)
policy_loading.setContentsMargins(0, 0, 0, 0)
policy_loading.setSpacing(10)
policy_loading.addWidget(_label('Policy Loading'))
self.policy_load_method = QComboBox()
self.policy_load_method.addItem('Resource Search', 'resource_search')
self.policy_load_method.addItem('Compartment-based Policy Get', 'compartment')
self.policy_load_method.setCurrentIndex(
max(0, self.policy_load_method.findData(self.settings.get('policy_load_method', 'resource_search')))
)
self.policy_load_method.setToolTip(
'Resource Search can lag recent changes. Compartment-based loading lists policies directly in each selected compartment, then fetches their details. The saved choice applies to live loads in either display mode.'
)
policy_loading.addWidget(self.policy_load_method, 1)
grid.addWidget(self.policy_loading_controls, 2, 3, 1, 3)
self.load_button = QPushButton('Load from OCI')
self.load_button.setObjectName('primary')
self.load_button.clicked.connect(self._load_oci)
grid.addWidget(self.load_button, 2, 0, 1, 2, alignment=Qt.AlignmentFlag.AlignLeft)
# Local snapshot panel: select a named cache, refresh the list, or open
# an exported JSON file directly.
saved = QVBoxLayout(self.source_panels[1])
saved.setContentsMargins(0, 8, 0, 0)
saved.addWidget(_label('Load a saved cache or open an exported JSON snapshot.', 'muted', wrap=True))
actions = QHBoxLayout()
self._responsive_rows.append(actions)
self.cache_list = QComboBox()
self.cache_list.setMinimumWidth(240)
actions.addWidget(self.cache_list, 1)
self.cache_button = QPushButton('Load cache')
self.cache_button.setObjectName('primary')
self.cache_button.clicked.connect(self._load_cache)
actions.addWidget(self.cache_button)
self.refresh_button = QPushButton('Refresh')
self.refresh_button.clicked.connect(self.refresh_caches)
actions.addWidget(self.refresh_button)
self.open_button = QPushButton('Open snapshot…')
self.open_button.clicked.connect(self._open_snapshot)
actions.addWidget(self.open_button)
saved.addLayout(actions)
self.refresh_caches()
# CIS import has its own user-loading checkbox but shares the same
# preference as the live tenancy option.
cis = QVBoxLayout(self.source_panels[2])
cis.setContentsMargins(0, 8, 0, 0)
cis.addWidget(
_label('Import the unzipped output directory from a CIS Compliance run with --raw.', 'muted', wrap=True)
)
cis_row = QHBoxLayout()
self._responsive_rows.append(cis_row)
self.cis_users = QCheckBox('Load all users')
self.cis_users.setChecked(self.load_all_users.isChecked())
self.cis_users.toggled.connect(self.load_all_users.setChecked)
self.load_all_users.toggled.connect(self.cis_users.setChecked)
cis_row.addWidget(self.cis_users)
self.cis_button = QPushButton('Load CIS output…')
self.cis_button.setObjectName('primary')
self.cis_button.clicked.connect(self._load_compliance)
cis_row.addWidget(self.cis_button)
cis_row.addStretch()
cis.addLayout(cis_row)
# Sample data is entirely local and does not need connection settings.
sample = QVBoxLayout(self.source_panels[3])
sample.setContentsMargins(0, 8, 0, 0)
sample.addWidget(
_label('Explore fictional policies and identities without connecting to OCI.', 'muted', wrap=True)
)
self.sample_button = QPushButton('Load sample data')
self.sample_button.setObjectName('primary')
self.sample_button.clicked.connect(self.load_sample)
sample_actions = QHBoxLayout()
sample_actions.addWidget(self.sample_button)
sample_actions.addStretch()
sample.addLayout(sample_actions)
self.source_selector.currentIndexChanged.connect(self._show_source)
saved_source = self.settings.get('qt_load_source', 'oci')
source_index = self.source_keys.index(saved_source) if saved_source in self.source_keys else 0
self.source_selector.setCurrentIndex(source_index)
self._show_source(source_index)
return card
def _show_source(self, index: int) -> None:
"""Show only the panel selected by the four-way source switch."""
for panel_index, panel in enumerate(self.source_panels):
panel.setVisible(panel_index == index)
@staticmethod
def _separator() -> QFrame:
"""Create the shared divider between the active data and settings."""
line = QFrame()
line.setObjectName('separator')
line.setFrameShape(QFrame.Shape.HLine)
return line
[docs]
def refresh_caches(self) -> None:
"""List saved caches without loading or rewriting a snapshot."""
previous = self.cache_list.currentData()
self.cache_list.clear()
try:
preserved = self.cache_service.get_preserved_cache_set()
for name in self.cache_service.list_caches():
self.cache_list.addItem(('(P) ' if name in preserved else '') + name, name)
if previous is not None:
self.cache_list.setCurrentIndex(max(0, self.cache_list.findData(previous)))
except OSError as exc:
self.cache_list.setToolTip(f'Could not list caches: {exc}')
if not self.cache_list.count():
self.cache_list.addItem('No saved caches', None)
self.cache_button.setEnabled(self.cache_list.currentData() is not None)
def _load_cache(self) -> None:
"""Request the currently selected saved cache from the load controller."""
name = self.cache_list.currentData()
if name:
self.load_requested.emit('cache', {'cache_name': name}, f'Cache: {name}')
def _load_compliance(self) -> None:
"""Choose a CIS raw-output folder and request its import."""
path = QFileDialog.getExistingDirectory(
self,
'Select CIS Compliance raw output directory',
'',
options=QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontUseNativeDialog,
)
if path:
self.load_requested.emit(
'compliance',
{'path': path, 'load_all_users': self.cis_users.isChecked()},
f'CIS output: {Path(path).name}',
)
def _active_data_card(self) -> QWidget:
"""Create the always-visible summary card for the active snapshot."""
card = QFrame()
card.setObjectName('card')
layout = QVBoxLayout(card)
layout.setContentsMargins(16, 14, 16, 14)
header = QHBoxLayout()
header.addWidget(_label('Active Data Set', 'eyebrow'))
header.addStretch()
self.load_status = _label(self._loaded_status, 'muted', wrap=True)
self.load_status.setMinimumWidth(560)
self.load_status.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
header.addWidget(self.load_status)
layout.addLayout(header)
self.active_data_layout = QVBoxLayout()
self.active_data_layout.setContentsMargins(0, 4, 0, 0)
layout.addLayout(self.active_data_layout)
return card
def _overview_page(self) -> QWidget:
"""Build snapshot counts and collection-coverage details."""
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(0, 0, 0, 0)
self.snapshot_label = _label('', 'eyebrow', wrap=True)
layout.addWidget(self.snapshot_label)
metrics = QHBoxLayout()
self._responsive_rows.append(metrics)
# Keep count labels keyed by stable result fields so publishing a new
# snapshot can update the cards without depending on visual order.
self.counts: dict[str, QLabel] = {}
for title, entries in [
('Policy data', [('policies', 'Policies'), ('statements', 'Statements'), ('invalid', 'Invalid')]),
('Identities', [('users', 'Users'), ('groups', 'Groups'), ('dynamic_groups', 'Dynamic groups')]),
(
'Resource principals · provisional',
[('resource_principal_statements', 'Any-user / any-group statements')],
),
('Scope', [('compartments', 'Compartments')]),
]:
card = QFrame()
card.setObjectName('card')
group = QVBoxLayout(card)
group.setContentsMargins(12, 8, 12, 8)
group.addWidget(_label(title, 'eyebrow', wrap=True))
columns = QHBoxLayout()
group.addLayout(columns)
for key, title in entries:
column = QVBoxLayout()
self.counts[key] = _label('—', 'metric')
column.addWidget(self.counts[key])
column.addWidget(_label(title, 'metricTitle', wrap=True))
columns.addLayout(column)
metrics.addWidget(card)
self.counts['invalid'].setToolTip(
'Source statements explicitly marked invalid; missing validity is not counted as invalid.'
)
self.counts['resource_principal_statements'].parentWidget().setToolTip(
'Provisional evidence count: source statements with subject type any-user or any-group. '
'Includes invalid statements with that parsed subject type. This is not a count of distinct '
'resource principals or proof that those statements apply to workloads.'
)
layout.addLayout(metrics)
self.coverage = QPlainTextEdit()
self.coverage.setReadOnly(True)
self.coverage.setFixedHeight(120)
layout.addWidget(self.coverage)
return page
[docs]
def reflow_rows(self) -> None:
"""Stack Overview control rows when text sizing exceeds the viewport width."""
available = max(200, self.viewport().width() - 32)
self.intelligence_panel.reflow_columns(available)
for row in self._responsive_rows:
required = sum(row.itemAt(i).sizeHint().width() for i in range(row.count()))
required += max(0, row.count() - 1) * max(6, row.spacing())
row.setDirection(
QBoxLayout.Direction.TopToBottom if required > available else QBoxLayout.Direction.LeftToRight
)
[docs]
def resizeEvent(self, event) -> None:
"""Reflow loading controls after the scroll viewport or font size changes."""
super().resizeEvent(event)
QTimer.singleShot(0, self.reflow_rows)
def _load_preferences_changed(self, *_args):
"""Debounce preference saves and flag changes to the loaded OCI source."""
self._load_preferences_dirty = True
if self._loaded_connection_signature is not None:
changed = self._connection_signature() != self._loaded_connection_signature
self._loaded_status = 'Loaded · OCI connection settings changed; reload to apply.' if changed else 'Loaded'
self._refresh_active_status()
self.preference_timer.start()
def _reload_profiles(self) -> None:
"""Refresh config profile names while preserving the current selection or typed name."""
try:
names = profile_names()
except (OSError, configparser.Error) as exc:
self.profile.setToolTip(f'Could not read OCI profiles: {exc}. Enter a profile name manually.')
return
selected = self.profile.currentText()
with QSignalBlocker(self.profile):
self.profile.clear()
self.profile.addItems(names)
self.profile.setCurrentText(selected)
self.profile.setToolTip('')
def _connection_signature(self) -> tuple[str, str]:
"""Return the authentication inputs used to identify an OCI connection."""
return (str(self.auth.currentData() or ''), self.profile.currentText().strip() or 'DEFAULT')
def _refresh_active_status(self) -> None:
"""Restore the saved load state when no load or lock message is active."""
if not self._busy and not self._load_locked:
self.load_status.setText(self._loaded_status)
[docs]
def save_load_preferences(self, *, force: bool = False) -> None:
"""Persist load source, authentication and scope, including edits before close.
Args:
force: Save an explicit load request even when controls are unchanged.
Notes:
Uses existing Tk/web setting keys and merges only these preferences.
Saving does not authenticate or collect tenancy data.
"""
self.preference_timer.stop()
if not force and not self._load_preferences_dirty:
return
self.settings.update(
{
'named_profile': self.profile.currentText().strip() or 'DEFAULT',
'qt_auth_mode': self.auth.currentData(),
'qt_load_source': self.source_keys[self.source_selector.currentIndex()],
'instance_principal': self.auth.currentData() == 'instance_principal',
'recursive': self.recursive.isChecked(),
'load_all_users': self.load_all_users.isChecked(),
'policy_load_method': self.policy_load_method.currentData(),
'domain_compartment_depth': self.depth.value(),
}
)
try:
save_preferences(
self.settings,
(
'named_profile',
'qt_auth_mode',
'qt_load_source',
'instance_principal',
'recursive',
'load_all_users',
'domain_compartment_depth',
'policy_load_method',
),
)
self._load_preferences_dirty = False
except OSError:
self._loaded_status = 'Connection preferences could not be saved.'
self._refresh_active_status()
[docs]
def load_options(self) -> dict:
"""Return explicit authentication and scope inputs without connecting to OCI."""
return {
'profile': self.profile.currentText().strip() or 'DEFAULT',
'auth': self.auth.currentData(),
'recursive': self.recursive.isChecked(),
'depth': self.depth.value(),
'load_all_users': self.load_all_users.isChecked(),
'policy_load_method': self.policy_load_method.currentData(),
}
[docs]
def load_sample(self) -> None:
"""Load fictional data through LoadService without authenticating to OCI."""
self.load_requested.emit('sample', {}, 'Synthetic sample · no OCI connection')
def _load_oci(self) -> None:
"""Save current options and emit a live OCI load request."""
self.save_load_preferences(force=True)
options = self.load_options()
description = (
f'OCI profile: {options["profile"]}'
if options['auth'] == 'profile'
else f'Session token profile: {options["profile"]}'
if options['auth'] == 'session_token'
else options['auth'].replace('_', ' ').title()
)
self.load_requested.emit('oci', options, description)
def _open_snapshot(self) -> None:
"""Choose an exported JSON snapshot and request it be loaded."""
path, _ = QFileDialog.getOpenFileName(
self,
'Open exported policy snapshot',
'',
'JSON snapshots (*.json)',
options=QFileDialog.Option.DontUseNativeDialog,
)
if path:
self.load_requested.emit('file', {'path': path}, f'Export: {Path(path).name}')
[docs]
def set_busy(self, busy: bool) -> None:
"""Disable load and logging inputs until the current worker finishes."""
self._busy = busy
self._update_controls()
[docs]
def set_load_locked(self, locked: bool) -> None:
"""Lock load sources while MCP serves the current snapshot."""
self._load_locked = locked
self._update_controls()
def _update_controls(self) -> None:
"""Enable or disable load controls based on worker and MCP state."""
busy = self._busy or self._load_locked
# Loading and source selection are locked during either state; optional
# settings panels only lock while a background load is active.
for widget in (
self.auth,
self.reload_profiles_button,
self.profile,
self.recursive,
self.load_all_users,
self.depth,
self.policy_load_method,
self.load_button,
self.open_button,
self.sample_button,
self.cache_list,
self.cache_button,
self.refresh_button,
self.cis_button,
self.cis_users,
):
widget.setEnabled(not busy)
self.profile.setEnabled(not busy and self.auth.currentData() in {'profile', 'session_token'})
self.cache_button.setEnabled(not busy and self.cache_list.currentData() is not None)
for panel in (self.logging_panel, self.intelligence_panel):
panel.setEnabled(not self._busy)
if self._busy:
self.load_status.setText('Loading… Previous results remain available.')
elif self._load_locked:
self.load_status.setText('Stop embedded MCP to load another snapshot.')
elif self.load_status.text() == 'Stop embedded MCP to load another snapshot.':
self._refresh_active_status()
[docs]
def set_snapshot(self, context: AppContext, result: LoadResult, description: str) -> None:
"""Refresh counts and collection scope from the newly published context."""
self.results.show()
self._loaded_connection_signature = (
self._connection_signature()
if description.startswith(('OCI profile:', 'Session token profile:')) or description == 'Instance Principal'
else None
)
self._loaded_status = 'Loaded'
self.load_status.setText(self._loaded_status)
repo = context.policy_repo
counts = {
**(result.summary or {}),
'users': len(repo.users or []),
'groups': len(repo.groups or []),
'dynamic_groups': len(repo.dynamic_groups or []),
'invalid': sum(statement.get('valid') is False for statement in repo.regular_statements),
'resource_principal_statements': sum(
str(statement.get('subject_type') or '').casefold() in {'any-user', 'any-group'}
for statement in repo.regular_statements
),
}
for key, value in self.counts.items():
value.setText(f'{counts.get(key, 0):,}')
self.snapshot_label.setText(f'{getattr(repo, "tenancy_name", None) or "Loaded snapshot"} / {description}')
recursion = getattr(repo, 'recursive', None)
traversal = {True: 'Root and descendants', False: 'Root only'}.get(recursion, 'Not recorded')
completeness = 'Reported complete' if getattr(repo, 'inventory_complete', None) is True else 'Not established'
warnings = getattr(repo, '_cleanup_reload_api_errors', []) or []
self.coverage.setPlainText(
f'Snapshot type: {getattr(repo, "snapshot_kind", None) or "Not recorded"}\n'
f'Policy collection scope: {traversal}\n'
f'Policy loading method: {getattr(repo, "policy_load_method", None) or "Not recorded"}\n'
f'Identity domain depth: {getattr(repo, "compartment_domain_search_depth", None) or "Not recorded"}\n'
f'Inventory completeness: {completeness}\n'
f'Collection errors reported: {len(warnings)} (not a completeness guarantee)\n'
'Detailed call results: View → Logs → API Calls (current application session only)\n'
f'Analysis: {context.settings.get("qt_post_load_profile", "minimal")}'
)