From b81bd9f913ddfcc609b939ec96a0572a903e1a59 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 21 Dec 2025 12:25:43 +0000 Subject: [PATCH] feat(ui): reduce AI Intelligence table length with 'Show more' buttons Added collapsible sections to prevent overwhelming list: - Dependencies limited to top 5 (sorted by confidence) - Actions limited to top 5 - Changes limited to top 5 - 'Show more' buttons appear at bottom when items are hidden - Clicking expands to show all items in that category This addresses user feedback about excessive scrolling when there are many dependency correlations or remediation actions. --- .../src/components/shared/AIOverviewTable.tsx | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/frontend-modern/src/components/shared/AIOverviewTable.tsx b/frontend-modern/src/components/shared/AIOverviewTable.tsx index b4609c3..cb997e7 100644 --- a/frontend-modern/src/components/shared/AIOverviewTable.tsx +++ b/frontend-modern/src/components/shared/AIOverviewTable.tsx @@ -41,8 +41,19 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = const [insightsLockedCount, setInsightsLockedCount] = createSignal(0); const [memoryLockedCount, setMemoryLockedCount] = createSignal(0); + // Section expand/collapse state - default limits prevent overwhelming list + const [showAllDependencies, setShowAllDependencies] = createSignal(false); + const [showAllActions, setShowAllActions] = createSignal(false); + const [showAllChanges, setShowAllChanges] = createSignal(false); + + // Limits for each section (when not expanded) + const DEPENDENCY_LIMIT = 5; + const ACTION_LIMIT = 5; + const CHANGE_LIMIT = 5; + const showWhenEmpty = () => Boolean(props.showWhenEmpty); + const loadData = async () => { setLoading(true); setError(''); @@ -190,8 +201,13 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = }); } - // Correlations - for (const corr of correlations()) { + // Correlations - sorted by confidence, limited unless expanded + const sortedCorrelations = [...correlations()].sort((a, b) => b.confidence - a.confidence); + const visibleCorrelations = showAllDependencies() + ? sortedCorrelations + : sortedCorrelations.slice(0, DEPENDENCY_LIMIT); + + for (const corr of visibleCorrelations) { rows.push({ id: `corr-${corr.source_id}-${corr.target_id}`, type: 'prediction', @@ -206,6 +222,7 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = }); } + // Remediations - ONLY show actual ACTIONS (restarts, resizes, cleanups, fixes) // Skip diagnostic commands (df, grep, cat, tail, ps) - they don't provide lasting value const isActionableCommand = (action: string): boolean => { @@ -231,8 +248,11 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = }; const actionableRemediations = remediations().filter(rem => isActionableCommand(rem.action)); + const visibleRemediations = showAllActions() + ? actionableRemediations + : actionableRemediations.slice(0, ACTION_LIMIT); - for (const rem of actionableRemediations) { + for (const rem of visibleRemediations) { const outcomeBadgeClass: Record = { resolved: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', partial: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', @@ -252,11 +272,16 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = }); } + // Changes - only show meaningful changes, not just "created" (infrastructure detection) // Valuable changes: config, status, migrated, restarted, deleted, backed_up // Skip: created (just means AI patrol discovered the resource) const meaningfulChanges = changes().filter(c => c.change_type !== 'created'); - for (const change of meaningfulChanges.slice(0, 6)) { + const visibleChanges = showAllChanges() + ? meaningfulChanges + : meaningfulChanges.slice(0, CHANGE_LIMIT); + + for (const change of visibleChanges) { const changeTypeBadgeClass: Record = { deleted: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', config: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', @@ -279,6 +304,7 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = }); } + return rows; }; @@ -522,6 +548,25 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = return stats; }; + // Calculate hidden items per section + const hiddenDependencies = () => { + const total = correlations().length; + return showAllDependencies() ? 0 : Math.max(0, total - DEPENDENCY_LIMIT); + }; + const hiddenActions = () => { + const actionable = remediations().filter(rem => { + const cmd = rem.action.trim().replace(/^\[[^\]]+\]\s*/, ''); + const patterns = ['docker restart', 'docker start', 'systemctl restart', 'pct resize', 'qm resize', 'rm -', 'chmod', 'chown', 'reboot']; + return patterns.some(p => cmd.includes(p)); + }); + return showAllActions() ? 0 : Math.max(0, actionable.length - ACTION_LIMIT); + }; + const hiddenChanges = () => { + const meaningful = changes().filter(c => c.change_type !== 'created'); + return showAllChanges() ? 0 : Math.max(0, meaningful.length - CHANGE_LIMIT); + }; + const anyHidden = () => hiddenDependencies() > 0 || hiddenActions() > 0 || hiddenChanges() > 0; + return (
@@ -668,7 +713,40 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = + {/* Show more buttons (if any hidden) */} + +
+
+ 0}> + + + 0}> + + + 0}> + + +
+
+
+ {/* Section locked notices (partial lock) */} +