Skip to content

Commit 6d3d2f8

Browse files
Add alert sidebar feature
1 parent 9b83fa3 commit 6d3d2f8

File tree

4 files changed

+129
-45
lines changed

4 files changed

+129
-45
lines changed

keep-ui/app/(keep)/alerts/[id]/ui/alerts.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { AlertDismissModal } from "@/features/alerts/dismiss-alert";
1313
import { ViewAlertModal } from "@/features/alerts/view-raw-alert";
1414
import { AlertChangeStatusModal } from "@/features/alerts/alert-change-status";
1515
import { EnrichAlertSidePanel } from "@/features/alerts/enrich-alert";
16+
import { AlertSidebar } from "@/features/alerts/alert-detail-sidebar";
17+
import { AlertAssociateIncidentModal } from "@/features/alerts/alert-associate-to-incident";
1618
import { FacetDto } from "@/features/filter";
1719
import { useApi } from "@/shared/lib/hooks/useApi";
1820
import { KeepLoader, showErrorToast } from "@/shared/ui";
@@ -75,6 +77,10 @@ export default function Alerts({ presetName, initialFacets }: AlertsProps) {
7577
const [viewEnrichAlertModal, setEnrichAlertModal] =
7678
useState<AlertDto | null>();
7779
const [isEnrichSidebarOpen, setIsEnrichSidebarOpen] = useState(false);
80+
// hooks for alert sidebar
81+
const [sidebarAlert, setSidebarAlert] = useState<AlertDto | null>();
82+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
83+
const [isIncidentSelectorOpen, setIsIncidentSelectorOpen] = useState(false);
7884
const { dynamicPresets: savedPresets = [], isLoading: _isPresetsLoading } =
7985
usePresets({
8086
revalidateOnFocus: false,
@@ -99,6 +105,8 @@ export default function Alerts({ presetName, initialFacets }: AlertsProps) {
99105
useEffect(() => {
100106
const fingerprint = searchParams?.get("alertPayloadFingerprint");
101107
const enrich = searchParams?.get("enrich");
108+
const sidebarFingerprint = searchParams?.get("sidebarFingerprint");
109+
102110
if (fingerprint && enrich && alerts) {
103111
const alert = alerts?.find((alert) => alert.fingerprint === fingerprint);
104112
if (alert) {
@@ -121,6 +129,21 @@ export default function Alerts({ presetName, initialFacets }: AlertsProps) {
121129
setEnrichAlertModal(null);
122130
setIsEnrichSidebarOpen(false);
123131
}
132+
133+
// Handle sidebar opening/closing based on URL parameter
134+
if (sidebarFingerprint && alerts) {
135+
const alert = alerts?.find((alert) => alert.fingerprint === sidebarFingerprint);
136+
if (alert) {
137+
setSidebarAlert(alert);
138+
setIsSidebarOpen(true);
139+
} else {
140+
showErrorToast(null, "Alert fingerprint not found");
141+
closeSidebar();
142+
}
143+
} else if (alerts) {
144+
setSidebarAlert(null);
145+
setIsSidebarOpen(false);
146+
}
124147
}, [searchParams, alerts]);
125148

126149
const alertsQueryStateRef = useRef(alertsQueryState);
@@ -146,7 +169,7 @@ export default function Alerts({ presetName, initialFacets }: AlertsProps) {
146169
const resetUrlAfterModal = useCallback(() => {
147170
const currentParams = new URLSearchParams(window.location.search);
148171
Array.from(currentParams.keys())
149-
.filter((paramKey) => paramKey !== "cel")
172+
.filter((paramKey) => paramKey !== "cel" && paramKey !== "sidebarFingerprint") // Keep sidebar parameter
150173
.forEach((paramKey) => currentParams.delete(paramKey));
151174
let url = `${window.location.pathname}`;
152175

@@ -157,6 +180,18 @@ export default function Alerts({ presetName, initialFacets }: AlertsProps) {
157180
router.replace(url);
158181
}, [router]);
159182

183+
const closeSidebar = useCallback(() => {
184+
const currentParams = new URLSearchParams(window.location.search);
185+
currentParams.delete("sidebarFingerprint"); // Only remove sidebar parameter
186+
let url = `${window.location.pathname}`;
187+
188+
if (currentParams.toString()) {
189+
url += `?${currentParams.toString()}`;
190+
}
191+
192+
router.replace(url);
193+
}, [router]);
194+
160195
// if we don't have presets data yet, just show loading
161196
if (!selectedPreset && isPresetsLoading) {
162197
return <KeepLoader />;
@@ -237,6 +272,24 @@ export default function Alerts({ presetName, initialFacets }: AlertsProps) {
237272
}}
238273
mutate={mutateAlerts}
239274
/>
275+
<AlertSidebar
276+
isOpen={isSidebarOpen}
277+
toggle={closeSidebar}
278+
alert={sidebarAlert ?? null}
279+
setRunWorkflowModalAlert={setRunWorkflowModalAlert}
280+
setDismissModalAlert={setDismissModalAlert}
281+
setChangeStatusAlert={setChangeStatusAlert}
282+
setIsIncidentSelectorOpen={setIsIncidentSelectorOpen}
283+
/>
284+
<AlertAssociateIncidentModal
285+
isOpen={isIncidentSelectorOpen}
286+
alerts={sidebarAlert ? [sidebarAlert] : []}
287+
handleSuccess={() => {
288+
setIsIncidentSelectorOpen(false);
289+
mutateAlerts();
290+
}}
291+
handleClose={() => setIsIncidentSelectorOpen(false)}
292+
/>
240293
</>
241294
);
242295
}

keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
EmptyStateCard,
2828
getCommonPinningStylesAndClassNames,
2929
} from "@/shared/ui";
30-
import { useRouter } from "next/navigation";
30+
import { useRouter, useSearchParams } from "next/navigation";
3131
import { TablePagination } from "@/shared/ui";
3232
import clsx from "clsx";
3333
import { IncidentAlertsTableBodySkeleton } from "./incident-alert-table-body-skeleton";
@@ -102,12 +102,46 @@ export default function IncidentAlerts({ incident }: Props) {
102102
const [viewAlertModal, setViewAlertModal] = useState<AlertDto | null>(null);
103103

104104
// State for AlertSidebar (opened by row click)
105-
const [selectedAlert, setSelectedAlert] = useState<AlertDto | null>(null);
105+
const [sidebarAlert, setSidebarAlert] = useState<AlertDto | null>(null);
106106
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
107107
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
108108

109109
// Add state for incident selector modal (needed by AlertSidebar)
110110
const [isIncidentSelectorOpen, setIsIncidentSelectorOpen] = useState(false);
111+
112+
const router = useRouter();
113+
const searchParams = useSearchParams();
114+
115+
// Handle sidebar opening/closing based on URL parameter
116+
useEffect(() => {
117+
const sidebarFingerprint = searchParams?.get("sidebarFingerprint");
118+
119+
if (sidebarFingerprint && alerts?.items) {
120+
const alert = alerts.items.find((alert) => alert.fingerprint === sidebarFingerprint);
121+
if (alert) {
122+
setSidebarAlert(alert);
123+
setIsSidebarOpen(true);
124+
} else {
125+
setSidebarAlert(null);
126+
setIsSidebarOpen(false);
127+
}
128+
} else {
129+
setSidebarAlert(null);
130+
setIsSidebarOpen(false);
131+
}
132+
}, [searchParams, alerts?.items]);
133+
134+
const closeSidebar = useCallback(() => {
135+
const currentParams = new URLSearchParams(window.location.search);
136+
currentParams.delete("sidebarFingerprint"); // Only remove sidebar parameter
137+
let url = `${window.location.pathname}`;
138+
139+
if (currentParams.toString()) {
140+
url += `?${currentParams.toString()}`;
141+
}
142+
143+
router.replace(url);
144+
}, [router]);
111145

112146
const extraColumns = [
113147
columnHelper.accessor("is_created_by_ai", {
@@ -276,12 +310,6 @@ export default function IncidentAlerts({ incident }: Props) {
276310
});
277311
}
278312

279-
// Handler for closing the sidebar
280-
const handleSidebarClose = () => {
281-
setIsSidebarOpen(false);
282-
setSelectedAlert(null);
283-
};
284-
285313
return (
286314
<>
287315
<IncidentAlertsActions
@@ -330,9 +358,10 @@ export default function IncidentAlerts({ incident }: Props) {
330358
showSkeleton={false}
331359
theme={theme}
332360
onRowClick={(alert) => {
333-
// Open the AlertSidebar when clicking on a row
334-
setSelectedAlert(alert);
335-
setIsSidebarOpen(true);
361+
// Open the AlertSidebar when clicking on a row via URL parameter
362+
const currentParams = new URLSearchParams(window.location.search);
363+
currentParams.set("sidebarFingerprint", alert.fingerprint);
364+
router.replace(`${window.location.pathname}?${currentParams.toString()}`);
336365
}}
337366
lastViewedAlert={null}
338367
presetName={"incident-alerts"}
@@ -361,8 +390,8 @@ export default function IncidentAlerts({ incident }: Props) {
361390
{/* AlertSidebar - opened by clicking on the alert row */}
362391
<AlertSidebar
363392
isOpen={isSidebarOpen}
364-
toggle={handleSidebarClose}
365-
alert={selectedAlert}
393+
toggle={closeSidebar}
394+
alert={sidebarAlert}
366395
// These optional props are passed to maintain feature parity with the main alerts table
367396
setRunWorkflowModalAlert={undefined}
368397
setDismissModalAlert={undefined}

keep-ui/features/alerts/alert-menu/ui/alert-menu.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ interface MenuItem {
6767
onClick: () => void;
6868
disabled?: boolean;
6969
show?: boolean;
70+
handlesOwnUrlTransition?: boolean; // If true, don't call toggleSidebar as the item manages URL itself
71+
keepsSidebarOpen?: boolean; // If true, don't close the sidebar when this action is clicked
7072
}
7173

7274
// Add the tooltip type
@@ -168,9 +170,14 @@ export function AlertMenu({
168170
}, [tooltipPosition]);
169171

170172
const updateUrl = useCallback(
171-
(params: { newParams?: Record<string, any>; scroll?: boolean }) => {
173+
(params: { newParams?: Record<string, any>; scroll?: boolean; removeParams?: string[] }) => {
172174
const currentParams = new URLSearchParams(window.location.search);
173175

176+
// Remove specified parameters first
177+
if (params.removeParams) {
178+
params.removeParams.forEach((paramKey) => currentParams.delete(paramKey));
179+
}
180+
174181
if (params.newParams) {
175182
Object.entries(params.newParams).forEach(([key, value]) =>
176183
currentParams.append(key, value)
@@ -205,8 +212,9 @@ export function AlertMenu({
205212

206213
updateUrl({
207214
newParams: { alertPayloadFingerprint: alert.fingerprint },
215+
removeParams: ["sidebarFingerprint"], // Close sidebar when opening modal
208216
});
209-
}, [alert, updateUrl]);
217+
}, [alert, updateUrl, setViewedAlerts]);
210218

211219
const actionIconButtonClassName = clsx(
212220
"text-gray-500 leading-none p-2 prevent-row-click hover:bg-slate-200 [&>[role='tooltip']]:z-50",
@@ -481,7 +489,11 @@ export function AlertMenu({
481489
icon: ArchiveBoxIcon,
482490
label: "History",
483491
onClick: () =>
484-
updateUrl({ newParams: { fingerprint: alert.fingerprint } }),
492+
updateUrl({
493+
newParams: { fingerprint: alert.fingerprint },
494+
removeParams: ["sidebarFingerprint"], // Close sidebar when opening modal
495+
}),
496+
handlesOwnUrlTransition: true,
485497
},
486498
{
487499
icon: AdjustmentsHorizontalIcon,
@@ -492,7 +504,9 @@ export function AlertMenu({
492504
alertPayloadFingerprint: alert.fingerprint,
493505
enrich: true,
494506
},
507+
removeParams: ["sidebarFingerprint"], // Close sidebar when opening modal
495508
}),
509+
handlesOwnUrlTransition: true,
496510
},
497511
{
498512
icon: UserPlusIcon,
@@ -504,6 +518,7 @@ export function AlertMenu({
504518
icon: EyeIcon,
505519
label: "View Alert",
506520
onClick: openAlertPayloadModal,
521+
handlesOwnUrlTransition: true,
507522
},
508523
...(provider?.methods?.map((method) => ({
509524
icon: (props: any) => (
@@ -534,6 +549,7 @@ export function AlertMenu({
534549
label: "Correlate Incident",
535550
onClick: () => setIsIncidentSelectorOpen?.(true),
536551
show: !!setIsIncidentSelectorOpen,
552+
keepsSidebarOpen: true, // Keep sidebar open so user can see alert details while selecting incident
537553
},
538554
],
539555
[
@@ -570,7 +586,12 @@ export function AlertMenu({
570586
key={item.label + index}
571587
onClick={() => {
572588
item.onClick();
573-
toggleSidebar?.();
589+
// Don't call toggleSidebar if:
590+
// 1. The item handles its own URL transition (will close sidebar via URL)
591+
// 2. The item explicitly wants to keep the sidebar open
592+
if (!item.handlesOwnUrlTransition && !item.keepsSidebarOpen) {
593+
toggleSidebar?.();
594+
}
574595
}}
575596
disabled={item.disabled}
576597
className="flex items-center space-x-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 disabled:opacity-50 rounded-tremor-default"

keep-ui/widgets/alerts-table/ui/alert-table-server-side.tsx

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
evalWithContext,
3636
} from "@/features/presets/presets-manager";
3737
import { severityMapping } from "@/entities/alerts/model";
38-
import { AlertSidebar } from "@/features/alerts/alert-detail-sidebar";
3938
import { useConfig } from "@/utils/hooks/useConfig";
4039
import { FacetsPanelServerSide } from "@/features/filter/facet-panel-server-side";
4140
import {
@@ -232,10 +231,11 @@ export function AlertTableServerSide({
232231
[filterCel, searchCel, paginationState, sorting, timeFrame, onQueryChange]
233232
);
234233

235-
const [selectedAlert, setSelectedAlert] = useState<AlertDto | null>(null);
236-
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
237234
const [isIncidentSelectorOpen, setIsIncidentSelectorOpen] =
238235
useState<boolean>(false);
236+
const router = useRouter();
237+
const pathname = usePathname();
238+
const searchParams = useSearchParams();
239239

240240
const leftPinnedColumns = noisyAlertsEnabled
241241
? ["severity", "checkbox", "status", "source", "noise"]
@@ -328,8 +328,11 @@ export function AlertTableServerSide({
328328
});
329329

330330
setLastViewedAlert(alert.fingerprint);
331-
setSelectedAlert(alert);
332-
setIsSidebarOpen(true);
331+
332+
// Open sidebar via URL parameter
333+
const currentParams = new URLSearchParams(window.location.search);
334+
currentParams.set("sidebarFingerprint", alert.fingerprint);
335+
router.replace(`${window.location.pathname}?${currentParams.toString()}`);
333336
};
334337

335338
const facetsConfig: FacetsConfig = useMemo(() => {
@@ -416,10 +419,7 @@ export function AlertTableServerSide({
416419

417420
const [isCreateIncidentWithAIOpen, setIsCreateIncidentWithAIOpen] =
418421
useState<boolean>(false);
419-
const router = useRouter();
420-
const pathname = usePathname();
421422
// handle "create incident with AI from last 25 alerts" if ?createIncidentsFromLastAlerts=25
422-
const searchParams = useSearchParams();
423423
useEffect(() => {
424424
if (alerts.length === 0 && selectedAlertsFingerprints.length) {
425425
return;
@@ -754,25 +754,6 @@ export function AlertTableServerSide({
754754
</div>
755755
</div>
756756

757-
<AlertSidebar
758-
isOpen={isSidebarOpen}
759-
toggle={() => setIsSidebarOpen(false)}
760-
alert={selectedAlert}
761-
setRunWorkflowModalAlert={setRunWorkflowModalAlert}
762-
setDismissModalAlert={setDismissModalAlert}
763-
setChangeStatusAlert={setChangeStatusAlert}
764-
setIsIncidentSelectorOpen={() => {
765-
if (selectedAlert) {
766-
table
767-
.getRowModel()
768-
.rows.find(
769-
(row) => row.original.fingerprint === selectedAlert.fingerprint
770-
)
771-
?.toggleSelected();
772-
setIsIncidentSelectorOpen(true);
773-
}
774-
}}
775-
/>
776757
</div>
777758
);
778759
}

0 commit comments

Comments
 (0)