mirror of
https://github.com/LittleQuartZ/addmon.git
synced 2026-02-07 02:45:28 +07:00
feat(monitor): add alert pairing logic and display incident names
- Add pair_alerts() to pair consecutive Alerting→Normal state transitions - Add parse_labels_from_text() to extract labels from alert text field - Update ProcessedAlert struct with alertTime, resolveTime, isResolved, labels - Display incident name in alert list items instead of incident ID - Show parsed labels in alert row (excluding alertname)
This commit is contained in:
parent
ea1c9105a7
commit
22aee93fb3
6 changed files with 165 additions and 129 deletions
|
|
@ -1,19 +1,17 @@
|
||||||
use crate::{calculate_kpis, AppError, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
|
use crate::{calculate_kpis, pair_alerts, AppError, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn load_alerts_from_file(path: String) -> Result<Vec<ProcessedAlert>, AppError> {
|
pub async fn load_alerts_from_file(path: String) -> Result<Vec<ProcessedAlert>, AppError> {
|
||||||
let contents = fs::read_to_string(&path)?;
|
let contents = fs::read_to_string(&path)?;
|
||||||
let raw_alerts: Vec<GrafanaAlert> = serde_json::from_str(&contents)?;
|
let raw_alerts: Vec<GrafanaAlert> = serde_json::from_str(&contents)?;
|
||||||
let processed: Vec<ProcessedAlert> = raw_alerts.into_iter().map(ProcessedAlert::from).collect();
|
Ok(pair_alerts(raw_alerts))
|
||||||
Ok(processed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn process_alerts_json(json_content: String) -> Result<Vec<ProcessedAlert>, AppError> {
|
pub fn process_alerts_json(json_content: String) -> Result<Vec<ProcessedAlert>, AppError> {
|
||||||
let raw_alerts: Vec<GrafanaAlert> = serde_json::from_str(&json_content)?;
|
let raw_alerts: Vec<GrafanaAlert> = serde_json::from_str(&json_content)?;
|
||||||
let processed: Vec<ProcessedAlert> = raw_alerts.into_iter().map(ProcessedAlert::from).collect();
|
Ok(pair_alerts(raw_alerts))
|
||||||
Ok(processed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
|
||||||
|
|
@ -98,44 +98,76 @@ pub struct ProcessedAlert {
|
||||||
pub alert_id: u32,
|
pub alert_id: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub alert_name: Option<String>,
|
pub alert_name: Option<String>,
|
||||||
pub new_state: String,
|
pub alert_time: u64,
|
||||||
pub prev_state: String,
|
pub resolve_time: Option<u64>,
|
||||||
pub time: u64,
|
|
||||||
pub time_end: u64,
|
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub duration_ms: u64,
|
pub duration_ms: u64,
|
||||||
pub is_firing: bool,
|
pub is_resolved: bool,
|
||||||
pub is_invalid: bool,
|
pub is_invalid: bool,
|
||||||
pub attached_incident_id: Option<String>,
|
pub attached_incident_id: Option<String>,
|
||||||
pub values: HashMap<String, f64>,
|
pub values: HashMap<String, f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GrafanaAlert> for ProcessedAlert {
|
pub fn pair_alerts(raw_alerts: Vec<GrafanaAlert>) -> Vec<ProcessedAlert> {
|
||||||
fn from(alert: GrafanaAlert) -> Self {
|
use std::collections::BTreeMap;
|
||||||
let duration_ms = if alert.time_end > alert.time {
|
|
||||||
alert.time_end - alert.time
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_firing = alert.new_state == "Alerting" || alert.new_state == "Firing";
|
let mut by_alert_id: BTreeMap<u32, Vec<GrafanaAlert>> = BTreeMap::new();
|
||||||
|
for alert in raw_alerts {
|
||||||
|
by_alert_id.entry(alert.alert_id).or_default().push(alert);
|
||||||
|
}
|
||||||
|
|
||||||
ProcessedAlert {
|
let mut result = Vec::new();
|
||||||
id: alert.id,
|
|
||||||
alert_id: alert.alert_id,
|
for (_alert_id, mut alerts) in by_alert_id {
|
||||||
alert_name: alert.alert_name,
|
alerts.sort_by_key(|a| a.time);
|
||||||
new_state: alert.new_state,
|
|
||||||
prev_state: alert.prev_state,
|
let mut i = 0;
|
||||||
time: alert.time,
|
while i < alerts.len() {
|
||||||
time_end: alert.time_end,
|
let current = &alerts[i];
|
||||||
text: alert.text,
|
|
||||||
duration_ms,
|
if current.new_state == "Alerting" {
|
||||||
is_firing,
|
if i + 1 < alerts.len() && alerts[i + 1].new_state == "Normal" {
|
||||||
is_invalid: false,
|
let resolve = &alerts[i + 1];
|
||||||
attached_incident_id: None,
|
let duration_ms = resolve.time.saturating_sub(current.time);
|
||||||
values: alert.data.map(|d| d.values).unwrap_or_default(),
|
|
||||||
|
result.push(ProcessedAlert {
|
||||||
|
id: current.id,
|
||||||
|
alert_id: current.alert_id,
|
||||||
|
alert_name: current.alert_name.clone(),
|
||||||
|
alert_time: current.time,
|
||||||
|
resolve_time: Some(resolve.time),
|
||||||
|
text: current.text.clone(),
|
||||||
|
duration_ms,
|
||||||
|
is_resolved: true,
|
||||||
|
is_invalid: false,
|
||||||
|
attached_incident_id: None,
|
||||||
|
values: current.data.clone().map(|d| d.values).unwrap_or_default(),
|
||||||
|
});
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
result.push(ProcessedAlert {
|
||||||
|
id: current.id,
|
||||||
|
alert_id: current.alert_id,
|
||||||
|
alert_name: current.alert_name.clone(),
|
||||||
|
alert_time: current.time,
|
||||||
|
resolve_time: None,
|
||||||
|
text: current.text.clone(),
|
||||||
|
duration_ms: 0,
|
||||||
|
is_resolved: false,
|
||||||
|
is_invalid: false,
|
||||||
|
attached_incident_id: None,
|
||||||
|
values: current.data.clone().map(|d| d.values).unwrap_or_default(),
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.sort_by(|a, b| b.alert_time.cmp(&a.alert_time));
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -176,19 +208,16 @@ pub fn calculate_kpis(alerts: &[ProcessedAlert], incidents: &[Incident]) -> KpiM
|
||||||
|
|
||||||
let overall_downtime_ms: u64 = alerts
|
let overall_downtime_ms: u64 = alerts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| a.is_firing && !a.is_invalid)
|
.filter(|a| !a.is_invalid)
|
||||||
.map(|a| a.duration_ms)
|
.map(|a| a.duration_ms)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
let overall_downtime_formatted = format_duration(overall_downtime_ms);
|
let overall_downtime_formatted = format_duration(overall_downtime_ms);
|
||||||
|
|
||||||
let total_firing_alerts = alerts.iter().filter(|a| a.is_firing).count();
|
let total_alerts = alerts.len();
|
||||||
let invalid_alerts = alerts
|
let invalid_alerts = alerts.iter().filter(|a| a.is_invalid).count();
|
||||||
.iter()
|
let invalid_alert_ratio = if total_alerts > 0 {
|
||||||
.filter(|a| a.is_firing && a.is_invalid)
|
(invalid_alerts as f64 / total_alerts as f64) * 100.0
|
||||||
.count();
|
|
||||||
let invalid_alert_ratio = if total_firing_alerts > 0 {
|
|
||||||
(invalid_alerts as f64 / total_firing_alerts as f64) * 100.0
|
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
@ -200,7 +229,7 @@ pub fn calculate_kpis(alerts: &[ProcessedAlert], incidents: &[Incident]) -> KpiM
|
||||||
overall_downtime_ms,
|
overall_downtime_ms,
|
||||||
overall_downtime_formatted,
|
overall_downtime_formatted,
|
||||||
invalid_alert_ratio,
|
invalid_alert_ratio,
|
||||||
total_firing_alerts,
|
total_firing_alerts: total_alerts,
|
||||||
invalid_alerts,
|
invalid_alerts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { cva } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Activity, Link as LinkIcon } from "lucide-react";
|
import { AlertTriangle, CheckCircle, Link as LinkIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
@ -19,19 +19,14 @@ function formatDuration(ms: number): string {
|
||||||
return `${seconds}s`;
|
return `${seconds}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeRange(start: number, end: number): string {
|
|
||||||
return `${format(start, "HH:mm:ss")} - ${format(end, "HH:mm:ss")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertItemVariants = cva(
|
const alertItemVariants = cva(
|
||||||
"group relative flex w-full items-center gap-3 border-b border-border p-3 transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
"group relative flex w-full items-center gap-3 border-b border-border p-3 transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
status: {
|
status: {
|
||||||
firing:
|
unresolved:
|
||||||
"bg-destructive/5 hover:bg-destructive/10 border-l-2 border-l-destructive",
|
"bg-destructive/5 hover:bg-destructive/10 border-l-2 border-l-destructive",
|
||||||
resolved: "border-l-2 border-l-transparent",
|
resolved: "border-l-2 border-l-green-500/50",
|
||||||
pending: "border-l-2 border-l-yellow-500/50",
|
|
||||||
},
|
},
|
||||||
invalid: {
|
invalid: {
|
||||||
true: "opacity-60 grayscale bg-muted/30",
|
true: "opacity-60 grayscale bg-muted/30",
|
||||||
|
|
@ -47,6 +42,7 @@ const alertItemVariants = cva(
|
||||||
|
|
||||||
interface AlertListItemProps {
|
interface AlertListItemProps {
|
||||||
alert: ProcessedAlert;
|
alert: ProcessedAlert;
|
||||||
|
incidentName?: string;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onToggleInvalid: (id: number, isInvalid: boolean) => void;
|
onToggleInvalid: (id: number, isInvalid: boolean) => void;
|
||||||
onSelect: (alert: ProcessedAlert) => void;
|
onSelect: (alert: ProcessedAlert) => void;
|
||||||
|
|
@ -56,12 +52,13 @@ interface AlertListItemProps {
|
||||||
export const AlertListItem = React.memo(
|
export const AlertListItem = React.memo(
|
||||||
({
|
({
|
||||||
alert,
|
alert,
|
||||||
|
incidentName,
|
||||||
isSelected,
|
isSelected,
|
||||||
onToggleInvalid,
|
onToggleInvalid,
|
||||||
onSelect,
|
onSelect,
|
||||||
style,
|
style,
|
||||||
}: AlertListItemProps) => {
|
}: AlertListItemProps) => {
|
||||||
const status = alert.isFiring ? "firing" : "resolved";
|
const status = alert.isResolved ? "resolved" : "unresolved";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -95,7 +92,7 @@ export const AlertListItem = React.memo(
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="truncate text-xs font-semibold text-foreground">
|
<span className="truncate text-xs font-semibold text-foreground">
|
||||||
{alert.alertName || `Unknown Alert #${alert.alertId}`}
|
{alert.alertName || `Alert #${alert.alertId}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex shrink-0 items-center gap-1 text-[10px] font-mono text-muted-foreground">
|
<span className="flex shrink-0 items-center gap-1 text-[10px] font-mono text-muted-foreground">
|
||||||
{alert.durationMs > 0 && (
|
{alert.durationMs > 0 && (
|
||||||
|
|
@ -106,35 +103,36 @@ export const AlertListItem = React.memo(
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
|
<div className="flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
{alert.isResolved ? (
|
||||||
className={cn(
|
<span className="flex items-center gap-1 text-green-600 dark:text-green-500">
|
||||||
"uppercase tracking-wider",
|
<CheckCircle className="size-3" />
|
||||||
alert.isFiring
|
Resolved
|
||||||
? "text-destructive font-bold"
|
</span>
|
||||||
: "text-green-600 dark:text-green-500",
|
) : (
|
||||||
)}
|
<span className="flex items-center gap-1 text-destructive font-bold">
|
||||||
>
|
<AlertTriangle className="size-3" />
|
||||||
{alert.newState}
|
Unresolved
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-border">|</span>
|
<span className="text-border">|</span>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{formatTimeRange(alert.time, alert.timeEnd)}
|
{format(alert.alertTime, "MM/dd HH:mm:ss")}
|
||||||
|
{alert.resolveTime && (
|
||||||
|
<> → {format(alert.resolveTime, "HH:mm:ss")}</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{alert.attachedIncidentId && (
|
{incidentName && (
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-0.5 text-primary"
|
className="flex items-center gap-0.5 text-primary"
|
||||||
title="Attached to incident"
|
title={`Attached to: ${incidentName}`}
|
||||||
>
|
>
|
||||||
<LinkIcon className="size-3" />
|
<LinkIcon className="size-3" />
|
||||||
<span className="font-mono">{alert.attachedIncidentId}</span>
|
<span className="max-w-24 truncate">{incidentName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{alert.isInvalid && (
|
|
||||||
<Activity className="size-3 text-muted-foreground/50" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -148,7 +146,7 @@ export const AlertListItem = React.memo(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{alert.isFiring && !alert.isInvalid && (
|
{!alert.isResolved && !alert.isInvalid && (
|
||||||
<div className="absolute right-0 top-0 h-full w-0.5 bg-destructive" />
|
<div className="absolute right-0 top-0 h-full w-0.5 bg-destructive" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ProcessedAlert } from "@/lib/types/alerts";
|
import type { Incident, ProcessedAlert } from "@/lib/types/alerts";
|
||||||
import { AlertListItem } from "./alert-list-item";
|
import { AlertListItem } from "./alert-list-item";
|
||||||
|
|
||||||
interface AlertListProps {
|
interface AlertListProps {
|
||||||
alerts: ProcessedAlert[];
|
alerts: ProcessedAlert[];
|
||||||
|
incidents: Incident[];
|
||||||
onToggleInvalid: (alertId: number, isInvalid: boolean) => void;
|
onToggleInvalid: (alertId: number, isInvalid: boolean) => void;
|
||||||
onSelectAlert: (alert: ProcessedAlert) => void;
|
onSelectAlert: (alert: ProcessedAlert) => void;
|
||||||
selectedAlertId?: number;
|
selectedAlertId?: number;
|
||||||
|
|
@ -14,6 +15,7 @@ interface AlertListProps {
|
||||||
|
|
||||||
export function AlertList({
|
export function AlertList({
|
||||||
alerts,
|
alerts,
|
||||||
|
incidents,
|
||||||
onToggleInvalid,
|
onToggleInvalid,
|
||||||
onSelectAlert,
|
onSelectAlert,
|
||||||
selectedAlertId,
|
selectedAlertId,
|
||||||
|
|
@ -21,6 +23,14 @@ export function AlertList({
|
||||||
}: AlertListProps) {
|
}: AlertListProps) {
|
||||||
const parentRef = React.useRef<HTMLDivElement>(null);
|
const parentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const incidentMap = React.useMemo(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const incident of incidents) {
|
||||||
|
map.set(incident.id, incident.title);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [incidents]);
|
||||||
|
|
||||||
const getItemKey = React.useCallback(
|
const getItemKey = React.useCallback(
|
||||||
(index: number) => alerts[index]?.id ?? index,
|
(index: number) => alerts[index]?.id ?? index,
|
||||||
[alerts],
|
[alerts],
|
||||||
|
|
@ -54,6 +64,11 @@ export function AlertList({
|
||||||
<AlertListItem
|
<AlertListItem
|
||||||
key={virtualItem.key}
|
key={virtualItem.key}
|
||||||
alert={alert}
|
alert={alert}
|
||||||
|
incidentName={
|
||||||
|
alert.attachedIncidentId
|
||||||
|
? incidentMap.get(alert.attachedIncidentId)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
isSelected={selectedAlertId === alert.id}
|
isSelected={selectedAlertId === alert.id}
|
||||||
onToggleInvalid={onToggleInvalid}
|
onToggleInvalid={onToggleInvalid}
|
||||||
onSelect={onSelectAlert}
|
onSelect={onSelectAlert}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ export interface ProcessedAlert {
|
||||||
id: number;
|
id: number;
|
||||||
alertId: number;
|
alertId: number;
|
||||||
alertName?: string;
|
alertName?: string;
|
||||||
newState: string;
|
alertTime: number;
|
||||||
prevState: string;
|
resolveTime: number | null;
|
||||||
time: number;
|
|
||||||
timeEnd: number;
|
|
||||||
text: string;
|
text: string;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
isFiring: boolean;
|
isResolved: boolean;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
attachedIncidentId: string | null;
|
attachedIncidentId: string | null;
|
||||||
values: Record<string, number>;
|
values: Record<string, number>;
|
||||||
|
|
|
||||||
|
|
@ -72,25 +72,17 @@ function MonitorPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleInvalid = async (alertId: number, isInvalid: boolean) => {
|
const handleToggleInvalid = (alertId: number, isInvalid: boolean) => {
|
||||||
try {
|
setAlerts((prev) => {
|
||||||
const updatedAlerts = await invoke<ProcessedAlert[]>(
|
const updated = prev.map((a) =>
|
||||||
"set_alert_invalid",
|
a.id === alertId ? { ...a, isInvalid } : a,
|
||||||
{
|
|
||||||
alerts,
|
|
||||||
alertId,
|
|
||||||
isInvalid,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
setAlerts(updatedAlerts);
|
recalculateKpis(updated, incidents);
|
||||||
await recalculateKpis(updatedAlerts, incidents);
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
if (selectedAlert?.id === alertId) {
|
if (selectedAlert?.id === alertId) {
|
||||||
const updated = updatedAlerts.find((a) => a.id === alertId);
|
setSelectedAlert((prev) => (prev ? { ...prev, isInvalid } : null));
|
||||||
if (updated) setSelectedAlert(updated);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -108,50 +100,55 @@ function MonitorPage() {
|
||||||
recalculateKpis(alerts, newIncidents);
|
recalculateKpis(alerts, newIncidents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAttachAlert = async (alertId: number, incidentId: string) => {
|
const handleAttachAlert = (alertId: number, incidentId: string) => {
|
||||||
try {
|
const updatedAlerts = alerts.map((a) =>
|
||||||
const [updatedAlerts, updatedIncidents] = await invoke<
|
a.id === alertId ? { ...a, attachedIncidentId: incidentId } : a,
|
||||||
[ProcessedAlert[], Incident[]]
|
);
|
||||||
>("attach_alert_to_incident", {
|
const updatedIncidents = incidents.map((i) =>
|
||||||
alerts,
|
i.id === incidentId && !i.attachedAlertIds.includes(alertId)
|
||||||
incidents,
|
? { ...i, attachedAlertIds: [...i.attachedAlertIds, alertId] }
|
||||||
alertId,
|
: i,
|
||||||
incidentId,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
setAlerts(updatedAlerts);
|
setAlerts(updatedAlerts);
|
||||||
setIncidents(updatedIncidents);
|
setIncidents(updatedIncidents);
|
||||||
await recalculateKpis(updatedAlerts, updatedIncidents);
|
recalculateKpis(updatedAlerts, updatedIncidents);
|
||||||
|
|
||||||
if (selectedAlert?.id === alertId) {
|
if (selectedAlert?.id === alertId) {
|
||||||
const updated = updatedAlerts.find((a) => a.id === alertId);
|
setSelectedAlert((prev) =>
|
||||||
if (updated) setSelectedAlert(updated);
|
prev ? { ...prev, attachedIncidentId: incidentId } : null,
|
||||||
}
|
);
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDetachAlert = async (alertId: number) => {
|
const handleDetachAlert = (alertId: number) => {
|
||||||
try {
|
const alert = alerts.find((a) => a.id === alertId);
|
||||||
const [updatedAlerts, updatedIncidents] = await invoke<
|
const incidentId = alert?.attachedIncidentId;
|
||||||
[ProcessedAlert[], Incident[]]
|
|
||||||
>("detach_alert_from_incident", {
|
|
||||||
alerts,
|
|
||||||
incidents,
|
|
||||||
alertId,
|
|
||||||
});
|
|
||||||
|
|
||||||
setAlerts(updatedAlerts);
|
const updatedAlerts = alerts.map((a) =>
|
||||||
setIncidents(updatedIncidents);
|
a.id === alertId ? { ...a, attachedIncidentId: null } : a,
|
||||||
await recalculateKpis(updatedAlerts, updatedIncidents);
|
);
|
||||||
|
const updatedIncidents = incidentId
|
||||||
|
? incidents.map((i) =>
|
||||||
|
i.id === incidentId
|
||||||
|
? {
|
||||||
|
...i,
|
||||||
|
attachedAlertIds: i.attachedAlertIds.filter(
|
||||||
|
(aid) => aid !== alertId,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: i,
|
||||||
|
)
|
||||||
|
: incidents;
|
||||||
|
|
||||||
if (selectedAlert?.id === alertId) {
|
setAlerts(updatedAlerts);
|
||||||
const updated = updatedAlerts.find((a) => a.id === alertId);
|
setIncidents(updatedIncidents);
|
||||||
if (updated) setSelectedAlert(updated);
|
recalculateKpis(updatedAlerts, updatedIncidents);
|
||||||
}
|
|
||||||
} catch (err) {
|
if (selectedAlert?.id === alertId) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setSelectedAlert((prev) =>
|
||||||
|
prev ? { ...prev, attachedIncidentId: null } : null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -197,6 +194,7 @@ function MonitorPage() {
|
||||||
) : (
|
) : (
|
||||||
<AlertList
|
<AlertList
|
||||||
alerts={alerts}
|
alerts={alerts}
|
||||||
|
incidents={incidents}
|
||||||
onToggleInvalid={handleToggleInvalid}
|
onToggleInvalid={handleToggleInvalid}
|
||||||
onSelectAlert={handleSelectAlert}
|
onSelectAlert={handleSelectAlert}
|
||||||
selectedAlertId={selectedAlert?.id}
|
selectedAlertId={selectedAlert?.id}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue