diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts-sidebar.test.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts-sidebar.test.tsx index addcb44a88..b93f75fac7 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts-sidebar.test.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts-sidebar.test.tsx @@ -169,18 +169,39 @@ let alertSidebarState = { // Mock the AlertSidebar to track its state jest.mock('@/features/alerts/alert-detail-sidebar', () => ({ - AlertSidebar: ({ isOpen, toggle, alert }: any) => { + AlertSidebar: ({ + isOpen, + toggle, + alert, + setRunWorkflowModalAlert, + setDismissModalAlert, + setChangeStatusAlert, + }: any) => { // Update our tracked state alertSidebarState.isOpen = isOpen; alertSidebarState.alert = alert; if (!isOpen) return null; + // Like AlertMenu in the sidebar: call the setter, then close the sidebar + const action = (label: string, onClick: () => void) => ( + + ); return (

{alert?.name || 'Alert Details'}

Severity: {alert?.severity}

+ {action('Run Workflow', () => setRunWorkflowModalAlert?.(alert))} + {action('Dismiss', () => setDismissModalAlert?.([alert]))} + {action('Change Status', () => setChangeStatusAlert?.(alert))} @@ -189,6 +210,36 @@ jest.mock('@/features/alerts/alert-detail-sidebar', () => ({ }, })); +jest.mock('@/features/workflows/manual-run-workflow', () => ({ + ManualRunWorkflowModal: ({ alert, onClose }: any) => + alert ? ( +
+ {alert.name} + +
+ ) : null, +})); + +jest.mock('@/features/alerts/dismiss-alert', () => ({ + AlertDismissModal: ({ alert, handleClose }: any) => + alert ? ( +
+ {alert[0].name} + +
+ ) : null, +})); + +jest.mock('@/features/alerts/alert-change-status', () => ({ + AlertChangeStatusModal: ({ alert, handleClose }: any) => + alert ? ( +
+ {alert.name} + +
+ ) : null, +})); + // Mock ViewAlertModal jest.mock("@/features/alerts/view-raw-alert", () => ({ ViewAlertModal: ({ alert, handleClose }: any) => @@ -420,6 +471,49 @@ describe('IncidentAlerts - AlertSidebar Integration', () => { expect(alertSidebarState.alert?.name).toBe('Test Alert 2'); }); + it.each([ + ['Run Workflow', 'run-workflow-modal'], + ['Dismiss', 'dismiss-modal'], + ['Change Status', 'change-status-modal'], + ])('should open the modal for the "%s" sidebar action', async (label, modalTestId) => { + render(); + + fireEvent.click(screen.getByTestId('alert-row-alert-1')); + fireEvent.click(await screen.findByRole('button', { name: label })); + + await waitFor(() => { + expect(screen.getByTestId(modalTestId)).toHaveTextContent('Test Alert 1'); + expect(screen.queryByTestId('alert-sidebar')).not.toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Close modal' })); + await waitFor(() => { + expect(screen.queryByTestId(modalTestId)).not.toBeInTheDocument(); + }); + }); + + it.each([ + ['Dismiss', 'dismiss-modal'], + ['Change Status', 'change-status-modal'], + ])('should refetch incident alerts when the "%s" modal closes', async (label, modalTestId) => { + const mutate = jest.fn(); + useIncidentAlerts.mockReturnValue({ + data: mockIncidentAlerts, + isLoading: false, + error: null, + mutate, + }); + + render(); + + fireEvent.click(screen.getByTestId('alert-row-alert-1')); + fireEvent.click(await screen.findByRole('button', { name: label })); + await screen.findByTestId(modalTestId); + fireEvent.click(screen.getByRole('button', { name: 'Close modal' })); + + expect(mutate).toHaveBeenCalled(); + }); + it('should show empty state when no alerts', () => { useIncidentAlerts.mockReturnValue({ data: { items: [], count: 0, limit: 20, offset: 0 }, diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts.test.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts.test.tsx index 754c1e2755..799dc054e3 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts.test.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/__tests__/incident-alerts.test.tsx @@ -89,6 +89,10 @@ jest.mock('@/features/alerts/alert-detail-sidebar', () => ({ }, })); +jest.mock('@/features/workflows/manual-run-workflow', () => ({ + ManualRunWorkflowModal: () => null, +})); + // Mock alert table utilities jest.mock('@/widgets/alerts-table/lib/alert-table-utils', () => ({ useAlertTableCols: jest.fn(() => [ diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx index 3efeed89f4..b51d4a9740 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx @@ -34,6 +34,9 @@ import { IncidentAlertsTableBodySkeleton } from "./incident-alert-table-body-ske import { IncidentAlertsActions } from "./incident-alert-actions"; import { AlertSidebar } from "@/features/alerts/alert-detail-sidebar"; import { ViewAlertModal } from "@/features/alerts/view-raw-alert"; +import { AlertChangeStatusModal } from "@/features/alerts/alert-change-status"; +import { AlertDismissModal } from "@/features/alerts/dismiss-alert"; +import { ManualRunWorkflowModal } from "@/features/workflows/manual-run-workflow"; import { IncidentAlertActionTray } from "./incident-alert-action-tray"; import { BellAlertIcon } from "@heroicons/react/24/outline"; import { AlertsTableBody } from "@/widgets/alerts-table/ui/alerts-table-body"; @@ -109,6 +112,15 @@ export default function IncidentAlerts({ incident }: Props) { // Add state for incident selector modal (needed by AlertSidebar) const [isIncidentSelectorOpen, setIsIncidentSelectorOpen] = useState(false); + const [runWorkflowModalAlert, setRunWorkflowModalAlert] = + useState(null); + const [dismissModalAlert, setDismissModalAlert] = useState< + AlertDto[] | null + >(null); + const [changeStatusAlert, setChangeStatusAlert] = useState( + null + ); + const extraColumns = [ columnHelper.accessor("is_created_by_ai", { id: "is_created_by_ai", @@ -363,12 +375,33 @@ export default function IncidentAlerts({ incident }: Props) { isOpen={isSidebarOpen} toggle={handleSidebarClose} alert={selectedAlert} - // These optional props are passed to maintain feature parity with the main alerts table - setRunWorkflowModalAlert={undefined} - setDismissModalAlert={undefined} - setChangeStatusAlert={undefined} + setRunWorkflowModalAlert={setRunWorkflowModalAlert} + setDismissModalAlert={setDismissModalAlert} + setChangeStatusAlert={setChangeStatusAlert} setIsIncidentSelectorOpen={setIsIncidentSelectorOpen} /> + + setRunWorkflowModalAlert(null)} + /> + {/* The modals revalidate only /alerts keys, so refetch the incident alerts on close */} + { + setDismissModalAlert(null); + mutateAlerts(); + }} + /> + { + setChangeStatusAlert(null); + mutateAlerts(); + }} + /> ); }