317 lines
9.0 KiB
TypeScript
317 lines
9.0 KiB
TypeScript
/**
|
|
* Unified Alert Management System
|
|
*
|
|
* Comprehensive system for handling all alert operations in the frontend
|
|
* including API calls, SSE processing, and UI state management
|
|
*/
|
|
|
|
import { Alert, Event, AlertTypeClass, PriorityLevel, EventDomain } from '../api/types/events';
|
|
import { translateAlertTitle, translateAlertMessage } from '../utils/alertI18n';
|
|
|
|
// ============================================================
|
|
// Type Definitions
|
|
// ============================================================
|
|
|
|
export interface AlertFilterOptions {
|
|
type_class?: AlertTypeClass[];
|
|
priority_level?: PriorityLevel[];
|
|
domain?: EventDomain[];
|
|
status?: ('active' | 'acknowledged' | 'resolved' | 'dismissed' | 'in_progress')[];
|
|
search?: string;
|
|
}
|
|
|
|
export interface AlertProcessingResult {
|
|
success: boolean;
|
|
alert?: Alert | AlertResponse;
|
|
error?: string;
|
|
}
|
|
|
|
// ============================================================
|
|
// Alert Processing Utilities
|
|
// ============================================================
|
|
|
|
/**
|
|
* Normalize alert to the unified structure (only for new Event structure)
|
|
*/
|
|
export function normalizeAlert(alert: any): Alert {
|
|
// Only accept the new Event structure - no legacy support
|
|
if (alert.event_class === 'alert') {
|
|
return alert as Alert;
|
|
}
|
|
|
|
// If it's an SSE EventSource message with nested data
|
|
if (alert.data && alert.data.event_class === 'alert') {
|
|
return alert.data as Alert;
|
|
}
|
|
|
|
throw new Error('Only new Event structure is supported by normalizeAlert');
|
|
}
|
|
|
|
/**
|
|
* Apply filters to an array of alerts
|
|
*/
|
|
export function applyAlertFilters(
|
|
alerts: Alert[],
|
|
filters: AlertFilterOptions = {},
|
|
search: string = ''
|
|
): Alert[] {
|
|
return alerts.filter(alert => {
|
|
// Filter by type class
|
|
if (filters.type_class && filters.type_class.length > 0) {
|
|
if (!alert.type_class || !filters.type_class.includes(alert.type_class as AlertTypeClass)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Filter by priority level
|
|
if (filters.priority_level && filters.priority_level.length > 0) {
|
|
if (!alert.priority_level || !filters.priority_level.includes(alert.priority_level as PriorityLevel)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Filter by domain
|
|
if (filters.domain && filters.domain.length > 0) {
|
|
if (!alert.event_domain || !filters.domain.includes(alert.event_domain as EventDomain)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Filter by status
|
|
if (filters.status && filters.status.length > 0) {
|
|
if (!alert.status || !filters.status.includes(alert.status as any)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Search filter
|
|
if (search) {
|
|
const searchLower = search.toLowerCase();
|
|
const title = translateAlertTitle(alert, (key: string, params?: any) => key) || '';
|
|
const message = translateAlertMessage(alert, (key: string, params?: any) => key) || '';
|
|
|
|
if (!title.toLowerCase().includes(searchLower) &&
|
|
!message.toLowerCase().includes(searchLower) &&
|
|
!alert.id.toLowerCase().includes(searchLower)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// Alert Filtering and Sorting
|
|
// ============================================================
|
|
|
|
/**
|
|
* Filter alerts based on provided criteria
|
|
*/
|
|
export function filterAlerts(alerts: Alert[], filters: AlertFilterOptions = {}): Alert[] {
|
|
return alerts.filter(alert => {
|
|
// Type class filter
|
|
if (filters.type_class && !filters.type_class.includes(alert.type_class)) {
|
|
return false;
|
|
}
|
|
|
|
// Priority level filter
|
|
if (filters.priority_level && !filters.priority_level.includes(alert.priority_level)) {
|
|
return false;
|
|
}
|
|
|
|
// Domain filter
|
|
if (filters.domain && !filters.domain.includes(alert.event_domain)) {
|
|
return false;
|
|
}
|
|
|
|
// Status filter
|
|
if (filters.status && !filters.status.includes(alert.status as any)) {
|
|
return false;
|
|
}
|
|
|
|
// Search filter
|
|
if (filters.search) {
|
|
const searchTerm = filters.search.toLowerCase();
|
|
const title = translateAlertTitle(alert, (key: string, params?: any) => key).toLowerCase();
|
|
const message = translateAlertMessage(alert, (key: string, params?: any) => key).toLowerCase();
|
|
|
|
if (!title.includes(searchTerm) && !message.includes(searchTerm)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sort alerts by priority, urgency, and creation time
|
|
*/
|
|
export function sortAlerts(alerts: Alert[]): Alert[] {
|
|
return [...alerts].sort((a, b) => {
|
|
// Sort by priority level first
|
|
const priorityOrder: Record<PriorityLevel, number> = {
|
|
critical: 4,
|
|
important: 3,
|
|
standard: 2,
|
|
info: 1
|
|
};
|
|
|
|
const priorityDiff = priorityOrder[b.priority_level] - priorityOrder[a.priority_level];
|
|
if (priorityDiff !== 0) return priorityDiff;
|
|
|
|
// If same priority, sort by type class
|
|
const typeClassOrder: Record<AlertTypeClass, number> = {
|
|
escalation: 5,
|
|
action_needed: 4,
|
|
prevented_issue: 3,
|
|
trend_warning: 2,
|
|
information: 1
|
|
};
|
|
|
|
const typeDiff = typeClassOrder[b.type_class] - typeClassOrder[a.type_class];
|
|
if (typeDiff !== 0) return typeDiff;
|
|
|
|
// If same type and priority, sort by creation time (newest first)
|
|
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// Alert Utility Functions
|
|
// ============================================================
|
|
|
|
/**
|
|
* Get alert icon based on type and priority
|
|
*/
|
|
export function getAlertIcon(alert: Alert): string {
|
|
switch (alert.type_class) {
|
|
case 'action_needed':
|
|
return alert.priority_level === 'critical' ? 'alert-triangle' : 'alert-circle';
|
|
case 'escalation':
|
|
return 'alert-triangle';
|
|
case 'trend_warning':
|
|
return 'trending-up';
|
|
case 'prevented_issue':
|
|
return 'check-circle';
|
|
case 'information':
|
|
default:
|
|
return 'info';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get alert color based on priority level
|
|
*/
|
|
export function getAlertColor(alert: Alert): string {
|
|
switch (alert.priority_level) {
|
|
case 'critical':
|
|
return 'var(--color-error)';
|
|
case 'important':
|
|
return 'var(--color-warning)';
|
|
case 'standard':
|
|
return 'var(--color-info)';
|
|
case 'info':
|
|
default:
|
|
return 'var(--color-success)';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if alert requires immediate attention
|
|
*/
|
|
export function requiresImmediateAttention(alert: Alert): boolean {
|
|
return alert.type_class === 'action_needed' &&
|
|
(alert.priority_level === 'critical' || alert.priority_level === 'important') &&
|
|
alert.status === 'active';
|
|
}
|
|
|
|
/**
|
|
* Check if alert is actionable (not already addressed)
|
|
*/
|
|
export function isActionable(alert: Alert): boolean {
|
|
return alert.status === 'active' &&
|
|
!alert.orchestrator_context?.already_addressed;
|
|
}
|
|
|
|
// ============================================================
|
|
// SSE Processing
|
|
// ============================================================
|
|
|
|
|
|
// ============================================================
|
|
// Alert State Management Utilities
|
|
// ============================================================
|
|
|
|
/**
|
|
* Merge new alerts with existing alerts, avoiding duplicates
|
|
*/
|
|
export function mergeAlerts(existingAlerts: Alert[], newAlerts: Alert[]): Alert[] {
|
|
const existingIds = new Set(existingAlerts.map(alert => alert.id));
|
|
const uniqueNewAlerts = newAlerts.filter(alert => !existingIds.has(alert.id));
|
|
|
|
return [...existingAlerts, ...uniqueNewAlerts];
|
|
}
|
|
|
|
/**
|
|
* Update specific alert in array (for status changes, etc.)
|
|
*/
|
|
export function updateAlertInArray(alerts: Alert[], updatedAlert: Alert): Alert[] {
|
|
return alerts.map(alert =>
|
|
alert.id === updatedAlert.id ? updatedAlert : alert
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove specific alert from array
|
|
*/
|
|
export function removeAlertFromArray(alerts: Alert[], alertId: string): Alert[] {
|
|
return alerts.filter(alert => alert.id !== alertId);
|
|
}
|
|
|
|
/**
|
|
* Get alert statistics
|
|
*/
|
|
export function getAlertStats(alerts: Alert[]) {
|
|
const stats = {
|
|
total: alerts.length,
|
|
active: 0,
|
|
acknowledged: 0,
|
|
resolved: 0,
|
|
critical: 0,
|
|
important: 0,
|
|
standard: 0,
|
|
info: 0,
|
|
actionNeeded: 0,
|
|
preventedIssue: 0,
|
|
trendWarning: 0,
|
|
escalation: 0,
|
|
information: 0
|
|
};
|
|
|
|
alerts.forEach(alert => {
|
|
switch (alert.status) {
|
|
case 'active': stats.active++; break;
|
|
case 'acknowledged': stats.acknowledged++; break;
|
|
case 'resolved': stats.resolved++; break;
|
|
}
|
|
|
|
switch (alert.priority_level) {
|
|
case 'critical': stats.critical++; break;
|
|
case 'important': stats.important++; break;
|
|
case 'standard': stats.standard++; break;
|
|
case 'info': stats.info++; break;
|
|
}
|
|
|
|
switch (alert.type_class) {
|
|
case 'action_needed': stats.actionNeeded++; break;
|
|
case 'prevented_issue': stats.preventedIssue++; break;
|
|
case 'trend_warning': stats.trendWarning++; break;
|
|
case 'escalation': stats.escalation++; break;
|
|
case 'information': stats.information++; break;
|
|
}
|
|
});
|
|
|
|
return stats;
|
|
} |