Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Header from './components/Header';
import { SignedIn, SignedOut } from '@clerk/clerk-react';
import { Routes, Route } from 'react-router-dom';
import { ClusterProvider } from './contexts/ClusterContext';
import { Toaster } from './components/ui/sonner';

const App = () => {
const [isDark, setIsDark] = useState(() => {
Expand Down Expand Up @@ -64,6 +65,15 @@ const App = () => {
{/* Scrollable content area with top padding to account for header */}
<div className="flex-1 overflow-y-auto pb-24 pt-16">
<div className="max-w-9xl mx-auto">
{/* SONNER TOAST NOTIFICATIONS */}
<Toaster
theme={isDark ? 'dark' : 'light'}
position="bottom-right"
richColors={true}
expand={false}
duration={5000}
closeButton={false}
/>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/historical" element={<HistoricalData />} />
Expand Down
61 changes: 60 additions & 1 deletion client/src/hooks/useNotifications.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';
import { toast } from 'sonner';

export interface K8sNotification {
id: string;
Expand All @@ -22,6 +23,51 @@ export const useNotifications = () => {
const [notifications, setNotifications] = useState<K8sNotification[]>([]);
const [isConnected, setIsConnected] = useState(false);

// Simple function to show Sonner toast - let Sonner handle all styling
const showToast = (notification: K8sNotification) => {
const { type, title, message, metadata, similarEventsCount } = notification;

// Create simple message content
const toastMessage = `${title}\n${metadata.namespace}/${metadata.pod}\n${message}${
similarEventsCount && similarEventsCount > 0
? `\n${similarEventsCount} similar events found`
: ''
}`;

// Let Sonner handle the colors and styling based on type
switch (type) {
case 'error':
if (metadata.severity === 'critical') {
toast.error(toastMessage, {
duration: 10000,
action: {
label: 'View Details',
onClick: () => {
console.log('Opening notification details:', notification.id);
},
},
});
} else {
toast.error(toastMessage, {
duration: 7000,
});
}
break;

case 'warning':
toast.warning(toastMessage, {
duration: 5000,
});
break;

default:
toast.info(toastMessage, {
duration: 4000,
});
break;
}
};

useEffect(() => {
const socketInstance = io(
import.meta.env.VITE_API_URL || 'http://localhost:3000',
Expand All @@ -30,19 +76,31 @@ export const useNotifications = () => {
socketInstance.on('connect', () => {
setIsConnected(true);
console.log('[WebSocket] Connected to server');

// Simple connection success toast
toast.success('Connected to Kubernetes monitoring');
});

socketInstance.on('disconnect', () => {
setIsConnected(false);
console.log('[WebSocket] Disconnected from server');

// Simple disconnection warning toast
toast.warning('Disconnected from monitoring server');
});

socketInstance.on('k8s-notification', (notification: K8sNotification) => {
console.log('[WebSocket] Received notification:', notification);

const newNotification = { ...notification, isRead: false };

setNotifications((prev) => [
{ ...notification, isRead: false },
newNotification,
...prev.slice(0, 49), // Keep only latest 50 notifications
]);

// Show Sonner toast notification
showToast(newNotification);
});

return () => {
Expand Down Expand Up @@ -72,6 +130,7 @@ export const useNotifications = () => {

const clearAllNotifications = () => {
setNotifications([]);
toast.success('All notifications cleared');
};

const unreadCount = notifications.filter((n) => !n.isRead).length;
Expand Down
54 changes: 49 additions & 5 deletions server/src/services/notificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,24 @@ interface K8sEvent {
timestamp: string;
}

// Extended notification interface for better toast control
interface ExtendedK8sNotification extends K8sNotification {
toastConfig?: {
duration?: number;
dismissible?: boolean;
showAction?: boolean;
priority?: 'low' | 'medium' | 'high' | 'critical';
};
}

export const sendNotifications = async (
event: K8sEvent,
aiResponse: string,
similarEvents: any,
) => {
try {
// Create unified notification object
const notification: K8sNotification = {
// Create unified notification object with toast configuration
const notification: ExtendedK8sNotification = {
id: uuidv4(),
type: determineNotificationType(event.reason),
title: `Kubernetes ${event.reason} Detected`,
Expand All @@ -35,11 +45,26 @@ export const sendNotifications = async (
},
aiAnalysis: aiResponse,
similarEventsCount: similarEvents.matches?.length || 0,
// Toast-specific configuration
toastConfig: {
duration: getToastDuration(event.reason),
dismissible: true,
showAction: shouldShowAction(event.reason),
priority: determineSeverity(event.reason) as
| 'low'
| 'medium'
| 'high'
| 'critical',
},
};

// Send to dashboard via WebSocket
// Send to dashboard via WebSocket (includes toast data)
broadcastNotification(notification);
console.log(chalk.green('[Notifications] Sent to dashboard via WebSocket'));
console.log(
chalk.green(
'[Notifications] Sent to dashboard via WebSocket with toast config',
),
);

// Send to Slack (existing functionality)
const slackMessage = formatSlackMessage(event, aiResponse, similarEvents);
Expand Down Expand Up @@ -76,6 +101,21 @@ const determineSeverity = (
return 'low';
};

// Helper function to determine toast duration based on event type
const getToastDuration = (reason: string): number => {
if (reason.includes('OOMKilled')) return 15000; // 15 seconds for critical
if (reason.includes('CrashLoopBackOff')) return 10000; // 10 seconds for high
if (reason.includes('Failed')) return 7000; // 7 seconds for medium
return 5000; // 5 seconds for low priority
};

// Helper function to determine if toast should show action button
const shouldShowAction = (reason: string): boolean => {
// Show action button for events that typically require immediate attention
const criticalReasons = ['OOMKilled', 'CrashLoopBackOff', 'Failed'];
return criticalReasons.some((r) => reason.includes(r));
};

const formatSlackMessage = (
event: K8sEvent,
aiResponse: string,
Expand All @@ -99,5 +139,9 @@ ${event.message}
*AI Analysis and Resolution:*
${aiResponse}

${similarEventCount > 0 ? `_Note: Found ${similarEventCount} similar past events that informed this analysis._` : '_Note: No similar past events found in the database._'}`;
${
similarEventCount > 0
? `_Note: Found ${similarEventCount} similar past events that informed this analysis._`
: '_Note: No similar past events found in the database._'
}`;
};