From 1b9894af84110972a9b33c1ad90e97c6828070a6 Mon Sep 17 00:00:00 2001 From: Sergei Proniakin Date: Thu, 1 Oct 2026 14:49:32 +0200 Subject: [PATCH] fix(ui): make alert sidebar actions work on the incident page On the incident page the alert sidebar got undefined for setChangeStatusAlert, setDismissModalAlert and setRunWorkflowModalAlert, and the page did not render the modals. "Change Status", "Dismiss" and "Run Workflow" closed the sidebar and did nothing. Keep the state for these actions and render the modals, as the alerts feed does. The dismiss and change-status modals revalidate only the /alert* SWR keys, so refetch the incident alerts when they close. Part of #5125 --- .../incident-alerts-sidebar.test.tsx | 96 ++++++++++++++++++- .../alerts/__tests__/incident-alerts.test.tsx | 4 + .../incidents/[id]/alerts/incident-alerts.tsx | 41 +++++++- 3 files changed, 136 insertions(+), 5 deletions(-) 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(); + }} + /> ); }