fix(ui): remove AI Intelligence Summary - patrol findings are sufficient
The AI Intelligence Summary was adding noise rather than value: - Predictions duplicated patrol findings - Correlations were not actionable - 'Fixed' items were vague diagnostics - Status changes were startup noise The real value is in the patrol findings section which shows: - Actual issues found (critical/warning/watch/info) - Actionable recommendations - Suppression rules Keeping the patrol findings, removing the redundant summary.
This commit is contained in:
parent
c8a32a7131
commit
4780dd2f83
1 changed files with 247 additions and 248 deletions
|
|
@ -7,7 +7,7 @@ import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';
|
||||||
import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton';
|
import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton';
|
||||||
import type { RawOverrideConfig, PMGThresholdDefaults, SnapshotAlertConfig, BackupAlertConfig } from '@/types/alerts';
|
import type { RawOverrideConfig, PMGThresholdDefaults, SnapshotAlertConfig, BackupAlertConfig } from '@/types/alerts';
|
||||||
import { Card } from '@/components/shared/Card';
|
import { Card } from '@/components/shared/Card';
|
||||||
import { AIOverviewTable } from '@/components/shared/AIOverviewTable';
|
|
||||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||||
import { Toggle } from '@/components/shared/Toggle';
|
import { Toggle } from '@/components/shared/Toggle';
|
||||||
|
|
@ -2817,9 +2817,8 @@ function OverviewTab(props: {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
{/* AI Intelligence Summary removed - patrol findings below provide the same value without noise */}
|
||||||
<AIOverviewTable showWhenEmpty />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={patrolRequiresLicense()}>
|
<Show when={patrolRequiresLicense()}>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|
@ -7250,260 +7249,260 @@ function HistoryTab() {
|
||||||
const rowKey = getIncidentRowKey(alert);
|
const rowKey = getIncidentRowKey(alert);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr
|
<tr
|
||||||
class={`border-b border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 ${alert.status === 'active' ? 'bg-red-50 dark:bg-red-900/10' : ''
|
class={`border-b border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 ${alert.status === 'active' ? 'bg-red-50 dark:bg-red-900/10' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
|
||||||
{/* Timestamp */}
|
|
||||||
<td class="p-1 px-2 text-gray-600 dark:text-gray-400 font-mono">
|
|
||||||
{new Date(alert.startTime).toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Source */}
|
|
||||||
<td class="p-1 px-2 text-center">
|
|
||||||
<span
|
|
||||||
class={`text-[10px] px-1.5 py-0.5 rounded font-medium ${alert.source === 'ai'
|
|
||||||
? 'bg-violet-100 dark:bg-violet-900/50 text-violet-700 dark:text-violet-300'
|
|
||||||
: 'bg-sky-100 dark:bg-sky-900/50 text-sky-700 dark:text-sky-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{alert.source === 'ai' ? 'AI' : 'Alert'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Resource */}
|
|
||||||
<td class="p-1 px-2 font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">
|
|
||||||
{alert.resourceName}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Type */}
|
|
||||||
<td class="p-1 px-2">
|
|
||||||
<span
|
|
||||||
class={`text-xs px-1 py-0.5 rounded ${alert.resourceType === 'VM'
|
|
||||||
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
|
|
||||||
: alert.resourceType === 'CT'
|
|
||||||
? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
|
|
||||||
: alert.resourceType === 'Node'
|
|
||||||
? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300'
|
|
||||||
: alert.resourceType === 'Storage'
|
|
||||||
? 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{alert.type}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Severity */}
|
|
||||||
<td class="p-1 px-2 text-center">
|
|
||||||
<span
|
|
||||||
class={`text-xs px-2 py-0.5 rounded font-medium ${alert.level === 'critical'
|
|
||||||
? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300'
|
|
||||||
: 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{alert.level}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Message */}
|
|
||||||
<td
|
|
||||||
class="p-1 px-2 text-gray-700 dark:text-gray-300 truncate max-w-[300px]"
|
|
||||||
title={alert.message}
|
|
||||||
>
|
>
|
||||||
{alert.message}
|
{/* Timestamp */}
|
||||||
</td>
|
<td class="p-1 px-2 text-gray-600 dark:text-gray-400 font-mono">
|
||||||
|
{new Date(alert.startTime).toLocaleTimeString('en-US', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
|
||||||
{/* Duration */}
|
{/* Source */}
|
||||||
<td class="p-1 px-2 text-center text-gray-600 dark:text-gray-400">
|
<td class="p-1 px-2 text-center">
|
||||||
{alert.duration}
|
<span
|
||||||
</td>
|
class={`text-[10px] px-1.5 py-0.5 rounded font-medium ${alert.source === 'ai'
|
||||||
|
? 'bg-violet-100 dark:bg-violet-900/50 text-violet-700 dark:text-violet-300'
|
||||||
|
: 'bg-sky-100 dark:bg-sky-900/50 text-sky-700 dark:text-sky-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{alert.source === 'ai' ? 'AI' : 'Alert'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
{/* Status */}
|
{/* Resource */}
|
||||||
<td class="p-1 px-2 text-center">
|
<td class="p-1 px-2 font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">
|
||||||
<span
|
{alert.resourceName}
|
||||||
class={`text-xs px-2 py-0.5 rounded ${alert.status === 'active'
|
</td>
|
||||||
? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300 font-medium'
|
|
||||||
: alert.status === 'acknowledged'
|
{/* Type */}
|
||||||
? 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300'
|
<td class="p-1 px-2">
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
<span
|
||||||
}`}
|
class={`text-xs px-1 py-0.5 rounded ${alert.resourceType === 'VM'
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
|
||||||
|
: alert.resourceType === 'CT'
|
||||||
|
? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
|
||||||
|
: alert.resourceType === 'Node'
|
||||||
|
? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300'
|
||||||
|
: alert.resourceType === 'Storage'
|
||||||
|
? 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{alert.type}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Severity */}
|
||||||
|
<td class="p-1 px-2 text-center">
|
||||||
|
<span
|
||||||
|
class={`text-xs px-2 py-0.5 rounded font-medium ${alert.level === 'critical'
|
||||||
|
? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300'
|
||||||
|
: 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{alert.level}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<td
|
||||||
|
class="p-1 px-2 text-gray-700 dark:text-gray-300 truncate max-w-[300px]"
|
||||||
|
title={alert.message}
|
||||||
>
|
>
|
||||||
{alert.status}
|
{alert.message}
|
||||||
</span>
|
</td>
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* Node */}
|
{/* Duration */}
|
||||||
<td class="p-1 px-2 text-gray-600 dark:text-gray-400 truncate">
|
<td class="p-1 px-2 text-center text-gray-600 dark:text-gray-400">
|
||||||
{alert.node || '—'}
|
{alert.duration}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Status */}
|
||||||
<td class="p-1 px-2 text-center">
|
<td class="p-1 px-2 text-center">
|
||||||
<div class="flex items-center justify-center gap-1">
|
<span
|
||||||
<Show when={alert.source === 'alert'}>
|
class={`text-xs px-2 py-0.5 rounded ${alert.status === 'active'
|
||||||
<button
|
? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300 font-medium'
|
||||||
type="button"
|
: alert.status === 'acknowledged'
|
||||||
class="px-2 py-1 text-[10px] border rounded-md border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
? 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300'
|
||||||
onClick={() => {
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
void toggleIncidentTimeline(rowKey, alert.id, alert.startTime);
|
}`}
|
||||||
}}
|
>
|
||||||
>
|
{alert.status}
|
||||||
{expandedIncidents().has(rowKey) ? 'Hide' : 'Timeline'}
|
</span>
|
||||||
</button>
|
</td>
|
||||||
</Show>
|
|
||||||
<Show when={alert.source === 'alert' && alert.resourceId}>
|
{/* Node */}
|
||||||
<button
|
<td class="p-1 px-2 text-gray-600 dark:text-gray-400 truncate">
|
||||||
type="button"
|
{alert.node || '—'}
|
||||||
class="px-2 py-1 text-[10px] border rounded-md border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
</td>
|
||||||
title="View incidents for this resource"
|
|
||||||
onClick={() => {
|
{/* Actions */}
|
||||||
void openResourceIncidentPanel(alert.resourceId as string, alert.resourceName);
|
<td class="p-1 px-2 text-center">
|
||||||
}}
|
<div class="flex items-center justify-center gap-1">
|
||||||
>
|
<Show when={alert.source === 'alert'}>
|
||||||
Resource
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</Show>
|
class="px-2 py-1 text-[10px] border rounded-md border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
<Show when={alert.source === 'alert' && (alert.status === 'active' || alert.status === 'acknowledged')}>
|
onClick={() => {
|
||||||
<InvestigateAlertButton
|
void toggleIncidentTimeline(rowKey, alert.id, alert.startTime);
|
||||||
alert={{
|
}}
|
||||||
id: alert.id,
|
>
|
||||||
type: alert.type,
|
{expandedIncidents().has(rowKey) ? 'Hide' : 'Timeline'}
|
||||||
level: alert.level as 'warning' | 'critical',
|
</button>
|
||||||
resourceId: alert.resourceId || '',
|
|
||||||
resourceName: alert.resourceName,
|
|
||||||
node: alert.node || '',
|
|
||||||
instance: '',
|
|
||||||
message: alert.message || '',
|
|
||||||
value: 0,
|
|
||||||
threshold: 0,
|
|
||||||
startTime: alert.startTime,
|
|
||||||
lastSeen: alert.startTime,
|
|
||||||
acknowledged: alert.status === 'acknowledged',
|
|
||||||
}}
|
|
||||||
variant="icon"
|
|
||||||
size="sm"
|
|
||||||
licenseLocked={!hasAIAlertsFeature() && !licenseLoading()}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<Show when={alert.source === 'alert' && expandedIncidents().has(rowKey)}>
|
|
||||||
<tr class="bg-gray-50 dark:bg-gray-900/40 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<td colspan="11" class="p-3">
|
|
||||||
<Show when={incidentLoading()[rowKey]}>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">Loading timeline...</p>
|
|
||||||
</Show>
|
|
||||||
<Show when={!incidentLoading()[rowKey]}>
|
|
||||||
<Show when={incidentTimelines()[rowKey]}>
|
|
||||||
{(timeline) => (
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
<span class="font-medium text-gray-700 dark:text-gray-200">Incident</span>
|
|
||||||
<span>{timeline().status}</span>
|
|
||||||
<Show when={timeline().acknowledged}>
|
|
||||||
<span class="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">
|
|
||||||
acknowledged
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={timeline().openedAt}>
|
|
||||||
<span>opened {new Date(timeline().openedAt).toLocaleString()}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={timeline().closedAt}>
|
|
||||||
<span>closed {new Date(timeline().closedAt as string).toLocaleString()}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
{(() => {
|
|
||||||
const events = timeline().events || [];
|
|
||||||
const filteredEvents = filterIncidentEvents(events, historyIncidentEventFilters());
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Show when={events.length > 0}>
|
|
||||||
<IncidentEventFilters
|
|
||||||
filters={historyIncidentEventFilters}
|
|
||||||
setFilters={setHistoryIncidentEventFilters}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<Show when={filteredEvents.length > 0}>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<For each={filteredEvents}>
|
|
||||||
{(event) => (
|
|
||||||
<div class="rounded border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/30 p-2">
|
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
<span class="font-medium text-gray-800 dark:text-gray-200">
|
|
||||||
{event.summary}
|
|
||||||
</span>
|
|
||||||
<span>{new Date(event.timestamp).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={event.details && (event.details as { note?: string }).note}>
|
|
||||||
<p class="text-xs text-gray-700 dark:text-gray-300 mt-1">
|
|
||||||
{(event.details as { note?: string }).note}
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
<Show when={event.details && (event.details as { command?: string }).command}>
|
|
||||||
<p class="text-xs text-gray-700 dark:text-gray-300 mt-1 font-mono">
|
|
||||||
{(event.details as { command?: string }).command}
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
<Show when={event.details && (event.details as { output_excerpt?: string }).output_excerpt}>
|
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{(event.details as { output_excerpt?: string }).output_excerpt}
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={events.length > 0 && filteredEvents.length === 0}>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
No timeline events match the selected filters.
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
<Show when={events.length === 0}>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">No timeline events yet.</p>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<textarea
|
|
||||||
class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 p-2 text-xs text-gray-800 dark:text-gray-200"
|
|
||||||
rows={2}
|
|
||||||
placeholder="Add a note for this incident..."
|
|
||||||
value={incidentNoteDrafts()[rowKey] || ''}
|
|
||||||
onInput={(e) => {
|
|
||||||
const value = e.currentTarget.value;
|
|
||||||
setIncidentNoteDrafts((prev) => ({ ...prev, [rowKey]: value }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button
|
|
||||||
class="px-3 py-1.5 text-xs font-medium border rounded-lg transition-all bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
disabled={incidentNoteSaving().has(rowKey) || !(incidentNoteDrafts()[rowKey] || '').trim()}
|
|
||||||
onClick={() => {
|
|
||||||
void saveIncidentNote(rowKey, alert.id, alert.startTime);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{incidentNoteSaving().has(rowKey) ? 'Saving...' : 'Save Note'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!incidentTimelines()[rowKey]}>
|
<Show when={alert.source === 'alert' && alert.resourceId}>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">No incident timeline available.</p>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 text-[10px] border rounded-md border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
|
title="View incidents for this resource"
|
||||||
|
onClick={() => {
|
||||||
|
void openResourceIncidentPanel(alert.resourceId as string, alert.resourceName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resource
|
||||||
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
<Show when={alert.source === 'alert' && (alert.status === 'active' || alert.status === 'acknowledged')}>
|
||||||
|
<InvestigateAlertButton
|
||||||
|
alert={{
|
||||||
|
id: alert.id,
|
||||||
|
type: alert.type,
|
||||||
|
level: alert.level as 'warning' | 'critical',
|
||||||
|
resourceId: alert.resourceId || '',
|
||||||
|
resourceName: alert.resourceName,
|
||||||
|
node: alert.node || '',
|
||||||
|
instance: '',
|
||||||
|
message: alert.message || '',
|
||||||
|
value: 0,
|
||||||
|
threshold: 0,
|
||||||
|
startTime: alert.startTime,
|
||||||
|
lastSeen: alert.startTime,
|
||||||
|
acknowledged: alert.status === 'acknowledged',
|
||||||
|
}}
|
||||||
|
variant="icon"
|
||||||
|
size="sm"
|
||||||
|
licenseLocked={!hasAIAlertsFeature() && !licenseLoading()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
<Show when={alert.source === 'alert' && expandedIncidents().has(rowKey)}>
|
||||||
</>
|
<tr class="bg-gray-50 dark:bg-gray-900/40 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<td colspan="11" class="p-3">
|
||||||
|
<Show when={incidentLoading()[rowKey]}>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">Loading timeline...</p>
|
||||||
|
</Show>
|
||||||
|
<Show when={!incidentLoading()[rowKey]}>
|
||||||
|
<Show when={incidentTimelines()[rowKey]}>
|
||||||
|
{(timeline) => (
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<span class="font-medium text-gray-700 dark:text-gray-200">Incident</span>
|
||||||
|
<span>{timeline().status}</span>
|
||||||
|
<Show when={timeline().acknowledged}>
|
||||||
|
<span class="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">
|
||||||
|
acknowledged
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={timeline().openedAt}>
|
||||||
|
<span>opened {new Date(timeline().openedAt).toLocaleString()}</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={timeline().closedAt}>
|
||||||
|
<span>closed {new Date(timeline().closedAt as string).toLocaleString()}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const events = timeline().events || [];
|
||||||
|
const filteredEvents = filterIncidentEvents(events, historyIncidentEventFilters());
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={events.length > 0}>
|
||||||
|
<IncidentEventFilters
|
||||||
|
filters={historyIncidentEventFilters}
|
||||||
|
setFilters={setHistoryIncidentEventFilters}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Show when={filteredEvents.length > 0}>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<For each={filteredEvents}>
|
||||||
|
{(event) => (
|
||||||
|
<div class="rounded border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/30 p-2">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<span class="font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
{event.summary}
|
||||||
|
</span>
|
||||||
|
<span>{new Date(event.timestamp).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={event.details && (event.details as { note?: string }).note}>
|
||||||
|
<p class="text-xs text-gray-700 dark:text-gray-300 mt-1">
|
||||||
|
{(event.details as { note?: string }).note}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<Show when={event.details && (event.details as { command?: string }).command}>
|
||||||
|
<p class="text-xs text-gray-700 dark:text-gray-300 mt-1 font-mono">
|
||||||
|
{(event.details as { command?: string }).command}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<Show when={event.details && (event.details as { output_excerpt?: string }).output_excerpt}>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{(event.details as { output_excerpt?: string }).output_excerpt}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={events.length > 0 && filteredEvents.length === 0}>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
No timeline events match the selected filters.
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<Show when={events.length === 0}>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">No timeline events yet.</p>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<textarea
|
||||||
|
class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 p-2 text-xs text-gray-800 dark:text-gray-200"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Add a note for this incident..."
|
||||||
|
value={incidentNoteDrafts()[rowKey] || ''}
|
||||||
|
onInput={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
setIncidentNoteDrafts((prev) => ({ ...prev, [rowKey]: value }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-xs font-medium border rounded-lg transition-all bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={incidentNoteSaving().has(rowKey) || !(incidentNoteDrafts()[rowKey] || '').trim()}
|
||||||
|
onClick={() => {
|
||||||
|
void saveIncidentNote(rowKey, alert.id, alert.startTime);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{incidentNoteSaving().has(rowKey) ? 'Saving...' : 'Save Note'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={!incidentTimelines()[rowKey]}>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">No incident timeline available.</p>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue