feat(ai): Add Suppression Rules UI to Alerts page
Users can now:
1. View all active suppression rules in a collapsible section
2. Add new rules manually with resource ID, category, and description
3. Delete rules to re-enable alerts
4. See whether rules came from dismissed findings or were manually created
The UI shows:
- 🔇 Suppression Rules (N active) header with expand/collapse
- + Add Rule button to open the form
- Each rule shows resource, category, origin (Manual/From Finding), and description
- Delete button to remove rules
This commit is contained in:
parent
a8d0b15346
commit
29404d4e81
1 changed files with 163 additions and 3 deletions
|
|
@ -28,7 +28,7 @@ import History from 'lucide-solid/icons/history';
|
|||
import Gauge from 'lucide-solid/icons/gauge';
|
||||
import Send from 'lucide-solid/icons/send';
|
||||
import Calendar from 'lucide-solid/icons/calendar';
|
||||
import { getPatrolStatus, getFindings, getFindingsHistory, getPatrolRunHistory, forcePatrol, subscribeToPatrolStream, dismissFinding, suppressFinding, type Finding, type PatrolStatus, type PatrolRunRecord, severityColors, formatTimestamp } from '@/api/patrol';
|
||||
import { getPatrolStatus, getFindings, getFindingsHistory, getPatrolRunHistory, forcePatrol, subscribeToPatrolStream, dismissFinding, suppressFinding, getSuppressionRules, addSuppressionRule, deleteSuppressionRule, type Finding, type PatrolStatus, type PatrolRunRecord, type SuppressionRule, severityColors, formatTimestamp, categoryLabels } from '@/api/patrol';
|
||||
import { aiChatStore } from '@/stores/aiChat';
|
||||
|
||||
type AlertTab = 'overview' | 'thresholds' | 'destinations' | 'schedule' | 'history';
|
||||
|
|
@ -2050,6 +2050,13 @@ function OverviewTab(props: {
|
|||
const [forcePatrolLoading, setForcePatrolLoading] = createSignal(false);
|
||||
const [expandedRunId, setExpandedRunId] = createSignal<string | null>(null);
|
||||
const [historyTimeFilter, setHistoryTimeFilter] = createSignal<'24h' | '7d' | 'all'>('all');
|
||||
// Suppression rules management
|
||||
const [suppressionRules, setSuppressionRules] = createSignal<SuppressionRule[]>([]);
|
||||
const [showSuppressionRules, setShowSuppressionRules] = createSignal(false);
|
||||
const [showAddRuleForm, setShowAddRuleForm] = createSignal(false);
|
||||
const [newRuleResource, setNewRuleResource] = createSignal('');
|
||||
const [newRuleCategory, setNewRuleCategory] = createSignal('');
|
||||
const [newRuleDescription, setNewRuleDescription] = createSignal('');
|
||||
// Live streaming state for running patrol
|
||||
const [expandedLiveStream, setExpandedLiveStream] = createSignal(false);
|
||||
// Track streaming blocks for sequential display (like AI chat)
|
||||
|
|
@ -2146,10 +2153,11 @@ function OverviewTab(props: {
|
|||
// Fetch AI data - extracted for reuse
|
||||
const fetchAiData = async () => {
|
||||
try {
|
||||
const [status, findings, runHistory] = await Promise.all([
|
||||
const [status, findings, runHistory, rules] = await Promise.all([
|
||||
getPatrolStatus(),
|
||||
getFindings(),
|
||||
getPatrolRunHistory(50) // Fetch more for filtering
|
||||
getPatrolRunHistory(50), // Fetch more for filtering
|
||||
getSuppressionRules().catch(() => []) // May not be available
|
||||
]);
|
||||
|
||||
// Check if a new patrol has completed - if so, clear pending fix findings
|
||||
|
|
@ -2165,6 +2173,7 @@ function OverviewTab(props: {
|
|||
setPatrolStatus(status);
|
||||
setAiFindings(findings);
|
||||
setPatrolRunHistory(runHistory);
|
||||
setSuppressionRules(rules);
|
||||
|
||||
// Auto-expand history if most recent run found issues
|
||||
if (runHistory.length > 0 && runHistory[0].status !== 'healthy') {
|
||||
|
|
@ -2743,6 +2752,157 @@ function OverviewTab(props: {
|
|||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Suppression Rules - What's being ignored */}
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||
onClick={() => setShowSuppressionRules(!showSuppressionRules())}
|
||||
>
|
||||
<svg
|
||||
class={`w-4 h-4 transition-transform ${showSuppressionRules() ? 'rotate-90' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
🔇 Suppression Rules ({suppressionRules().length} active)
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
||||
onClick={() => setShowAddRuleForm(!showAddRuleForm())}
|
||||
>
|
||||
+ Add Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add rule form */}
|
||||
<Show when={showAddRuleForm()}>
|
||||
<div class="mt-3 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-3">
|
||||
Add a suppression rule to prevent alerts
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
placeholder="Resource ID (or leave empty for any)"
|
||||
value={newRuleResource()}
|
||||
onInput={(e) => setNewRuleResource(e.currentTarget.value)}
|
||||
/>
|
||||
<select
|
||||
class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
value={newRuleCategory()}
|
||||
onChange={(e) => setNewRuleCategory(e.currentTarget.value)}
|
||||
>
|
||||
<option value="">Any category</option>
|
||||
<option value="performance">Performance</option>
|
||||
<option value="capacity">Capacity</option>
|
||||
<option value="reliability">Reliability</option>
|
||||
<option value="backup">Backup</option>
|
||||
<option value="security">Security</option>
|
||||
<option value="general">General</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
placeholder="Reason (e.g., 'Dev container runs hot')"
|
||||
value={newRuleDescription()}
|
||||
onInput={(e) => setNewRuleDescription(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
disabled={!newRuleDescription()}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await addSuppressionRule(
|
||||
newRuleResource(),
|
||||
newRuleResource() || 'Any resource',
|
||||
(newRuleCategory() as 'performance' | 'capacity' | 'reliability' | 'backup' | 'security' | 'general' | ''),
|
||||
newRuleDescription()
|
||||
);
|
||||
showSuccess('Suppression rule created');
|
||||
setShowAddRuleForm(false);
|
||||
setNewRuleResource('');
|
||||
setNewRuleCategory('');
|
||||
setNewRuleDescription('');
|
||||
fetchAiData();
|
||||
} catch (err) {
|
||||
showError('Failed to create rule');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create Rule
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium rounded bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
||||
onClick={() => setShowAddRuleForm(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Rules list */}
|
||||
<Show when={showSuppressionRules()}>
|
||||
<div class="mt-3 space-y-2">
|
||||
<Show when={suppressionRules().length === 0}>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 italic">
|
||||
No suppression rules. Dismiss findings or add rules to prevent unwanted alerts.
|
||||
</p>
|
||||
</Show>
|
||||
<For each={suppressionRules()}>
|
||||
{(rule) => (
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="font-medium text-gray-800 dark:text-gray-200">
|
||||
{rule.resource_name || rule.resource_id || 'Any resource'}
|
||||
</span>
|
||||
<Show when={rule.category}>
|
||||
<span class="px-2 py-0.5 text-xs rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
{categoryLabels[rule.category!] || rule.category}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={!rule.category}>
|
||||
<span class="px-2 py-0.5 text-xs rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
Any category
|
||||
</span>
|
||||
</Show>
|
||||
<span class="px-1.5 py-0.5 text-xs rounded bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">
|
||||
{rule.created_from === 'finding' ? 'From Finding' : 'Manual'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1 truncate">
|
||||
{rule.description || 'No description'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="ml-3 px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteSuppressionRule(rule.id);
|
||||
showSuccess('Rule deleted');
|
||||
fetchAiData();
|
||||
} catch (err) {
|
||||
showError('Failed to delete rule');
|
||||
}
|
||||
}}
|
||||
title="Delete this rule"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Patrol Check History */}
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
|
|||
Loading…
Reference in a new issue