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.
This commit is contained in:
parent
0730f857a4
commit
b81bd9f913
1 changed files with 82 additions and 4 deletions
|
|
@ -41,8 +41,19 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) =
|
||||||
const [insightsLockedCount, setInsightsLockedCount] = createSignal(0);
|
const [insightsLockedCount, setInsightsLockedCount] = createSignal(0);
|
||||||
const [memoryLockedCount, setMemoryLockedCount] = 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 showWhenEmpty = () => Boolean(props.showWhenEmpty);
|
||||||
|
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
@ -190,8 +201,13 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) =
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correlations
|
// Correlations - sorted by confidence, limited unless expanded
|
||||||
for (const corr of correlations()) {
|
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({
|
rows.push({
|
||||||
id: `corr-${corr.source_id}-${corr.target_id}`,
|
id: `corr-${corr.source_id}-${corr.target_id}`,
|
||||||
type: 'prediction',
|
type: 'prediction',
|
||||||
|
|
@ -206,6 +222,7 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) =
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Remediations - ONLY show actual ACTIONS (restarts, resizes, cleanups, fixes)
|
// Remediations - ONLY show actual ACTIONS (restarts, resizes, cleanups, fixes)
|
||||||
// Skip diagnostic commands (df, grep, cat, tail, ps) - they don't provide lasting value
|
// Skip diagnostic commands (df, grep, cat, tail, ps) - they don't provide lasting value
|
||||||
const isActionableCommand = (action: string): boolean => {
|
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 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<string, string> = {
|
const outcomeBadgeClass: Record<string, string> = {
|
||||||
resolved: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
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',
|
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)
|
// Changes - only show meaningful changes, not just "created" (infrastructure detection)
|
||||||
// Valuable changes: config, status, migrated, restarted, deleted, backed_up
|
// Valuable changes: config, status, migrated, restarted, deleted, backed_up
|
||||||
// Skip: created (just means AI patrol discovered the resource)
|
// Skip: created (just means AI patrol discovered the resource)
|
||||||
const meaningfulChanges = changes().filter(c => c.change_type !== 'created');
|
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<string, string> = {
|
const changeTypeBadgeClass: Record<string, string> = {
|
||||||
deleted: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
|
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',
|
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;
|
return rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -522,6 +548,25 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) =
|
||||||
return stats;
|
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 (
|
return (
|
||||||
<Show when={shouldShow()}>
|
<Show when={shouldShow()}>
|
||||||
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden shadow-sm">
|
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden shadow-sm">
|
||||||
|
|
@ -668,7 +713,40 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) =
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Show more buttons (if any hidden) */}
|
||||||
|
<Show when={!loading() && anyHidden()}>
|
||||||
|
<div class="px-4 py-2 border-t border-gray-100 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/30">
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-xs">
|
||||||
|
<Show when={hiddenDependencies() > 0}>
|
||||||
|
<button
|
||||||
|
class="text-indigo-600 dark:text-indigo-400 hover:underline"
|
||||||
|
onClick={() => setShowAllDependencies(true)}
|
||||||
|
>
|
||||||
|
+ {hiddenDependencies()} more dependencies
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={hiddenActions() > 0}>
|
||||||
|
<button
|
||||||
|
class="text-emerald-600 dark:text-emerald-400 hover:underline"
|
||||||
|
onClick={() => setShowAllActions(true)}
|
||||||
|
>
|
||||||
|
+ {hiddenActions()} more actions
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={hiddenChanges() > 0}>
|
||||||
|
<button
|
||||||
|
class="text-teal-600 dark:text-teal-400 hover:underline"
|
||||||
|
onClick={() => setShowAllChanges(true)}
|
||||||
|
>
|
||||||
|
+ {hiddenChanges()} more changes
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Section locked notices (partial lock) */}
|
{/* Section locked notices (partial lock) */}
|
||||||
|
|
||||||
<Show when={!loading() && anyLocked() && !(insightsLocked() && impactLocked() && memoryLocked())}>
|
<Show when={!loading() && anyLocked() && !(insightsLocked() && impactLocked() && memoryLocked())}>
|
||||||
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-amber-50/50 dark:bg-amber-900/10">
|
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-amber-50/50 dark:bg-amber-900/10">
|
||||||
<div class="flex flex-wrap items-center gap-4 text-xs">
|
<div class="flex flex-wrap items-center gap-4 text-xs">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue