Skip to content

Commit 667cc7c

Browse files
authored
chore: several improvements based on feedback (keephq#3576)
1 parent d503cc1 commit 667cc7c

File tree

15 files changed

+214
-63
lines changed

15 files changed

+214
-63
lines changed

keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import {
99
} from "../../../utils/hooks/useIncidents";
1010
import Loading from "@/app/(keep)/loading";
1111
import { AlertDto } from "@/entities/alerts/model";
12-
import { getIncidentName } from "@/entities/incidents/lib/utils";
12+
import {
13+
getIncidentName,
14+
getIncidentNameWithCreationTime,
15+
} from "@/entities/incidents/lib/utils";
1316
import { useApi } from "@/shared/lib/hooks/useApi";
1417
import { Select, showErrorToast } from "@/shared/ui";
18+
import { IncidentDto, Status } from "@/entities/incidents/model";
1519

1620
interface AlertAssociateIncidentModalProps {
1721
isOpen: boolean;
@@ -77,6 +81,13 @@ const AlertAssociateIncidentModal = ({
7781
[associateAlertsHandler, handleClose, hideCreateIncidentForm]
7882
);
7983

84+
const filterIncidents = (incident: IncidentDto) => {
85+
return (
86+
incident.status === Status.Firing ||
87+
incident.status === Status.Acknowledged
88+
);
89+
};
90+
8091
// reset modal state after closing
8192
useEffect(() => {
8293
if (!isOpen) {
@@ -127,9 +138,9 @@ const AlertAssociateIncidentModal = ({
127138
onChange={(selectedOption) =>
128139
setSelectedIncident(selectedOption?.value)
129140
}
130-
options={incidents.items?.map((incident) => ({
141+
options={incidents.items?.filter(filterIncidents).map((incident) => ({
131142
value: incident.id,
132-
label: getIncidentName(incident),
143+
label: getIncidentNameWithCreationTime(incident),
133144
}))}
134145
/>
135146
<Divider />

keep-ui/app/(keep)/alerts/alert-grouped-row.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1-
import { TableBody, TableRow, TableCell } from "@tremor/react";
1+
import { TableBody, TableRow, TableCell, Icon } from "@tremor/react";
22
import { AlertDto, Severity } from "@/entities/alerts/model";
33
import { Table, flexRender, Row } from "@tanstack/react-table";
4-
import { ChevronDownIcon } from "@heroicons/react/24/outline";
4+
import { ChevronDownIcon, EyeIcon } from "@heroicons/react/24/outline";
55
import clsx from "clsx";
66
import { useState } from "react";
77
import {
88
TableSeverityCell,
99
UISeverity,
1010
getCommonPinningStylesAndClassNames,
1111
} from "@/shared/ui";
12+
import { ViewedAlert } from "./alert-table";
13+
import { format } from "date-fns";
1214

1315
interface GroupedRowProps {
1416
row: Row<AlertDto>;
1517
table: Table<AlertDto>;
1618
theme: Record<string, string>;
1719
onRowClick?: (e: React.MouseEvent, alert: AlertDto) => void;
20+
viewedAlerts: ViewedAlert[];
21+
lastViewedAlert: string | null;
1822
}
1923

2024
export const GroupedRow = ({
2125
row,
2226
table,
2327
theme,
2428
onRowClick,
29+
viewedAlerts,
30+
lastViewedAlert,
2531
}: GroupedRowProps) => {
2632
const [isExpanded, setIsExpanded] = useState(true);
2733

@@ -31,10 +37,6 @@ export const GroupedRow = ({
3137
? row.getValue(groupingColumnId)
3238
: "Unknown Group";
3339

34-
const groupColumnIndex = row
35-
.getVisibleCells()
36-
.findIndex((cell) => cell.column.id === groupingColumnId);
37-
3840
return (
3941
<>
4042
{/* Group Header Row */}
@@ -109,6 +111,10 @@ export const GroupedRow = ({
109111
// Regular non-grouped row
110112
const severity = row.original.severity || "info";
111113
const rowBgColor = theme[severity] || "bg-white";
114+
const isLastViewed = row.original.fingerprint === lastViewedAlert;
115+
const viewedAlert = viewedAlerts?.find(
116+
(a) => a.fingerprint === row.original.fingerprint
117+
);
112118
return (
113119
<TableRow
114120
id={`alert-row-${row.original.fingerprint}`}
@@ -129,11 +135,26 @@ export const GroupedRow = ({
129135
cell.column.columnDef.meta?.tdClassName,
130136
className,
131137
rowBgColor,
132-
"group-hover:bg-orange-100"
138+
"group-hover:bg-orange-100",
139+
isLastViewed && "bg-orange-50"
133140
)}
134141
style={style}
135142
>
136-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
143+
{viewedAlert && cell.column.id === "alertMenu" ? (
144+
<div className="flex items-center gap-2">
145+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
146+
147+
<Icon
148+
icon={EyeIcon}
149+
tooltip={`Viewed ${format(
150+
new Date(viewedAlert.viewedAt),
151+
"MMM d, yyyy HH:mm"
152+
)}`}
153+
/>
154+
</div>
155+
) : (
156+
flexRender(cell.column.columnDef.cell, cell.getContext())
157+
)}
137158
</TableCell>
138159
);
139160
})}

keep-ui/app/(keep)/alerts/alert-table-server-side.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,13 @@ import { Icon } from "@tremor/react";
4646
import { BellIcon, BellSlashIcon } from "@heroicons/react/24/outline";
4747
import AlertPaginationServerSide from "./alert-pagination-server-side";
4848
import { FacetDto } from "@/features/filter";
49-
import {
50-
GroupingState,
51-
getGroupedRowModel,
52-
getExpandedRowModel,
53-
} from "@tanstack/react-table";
49+
import { GroupingState, getGroupedRowModel } from "@tanstack/react-table";
5450
import { TimeFrame } from "@/components/ui/DateRangePicker";
5551
import { AlertsQuery } from "@/utils/hooks/useAlerts";
5652
import { v4 as uuidV4 } from "uuid";
5753
import { FacetsConfig } from "@/features/filter/models";
54+
import { ViewedAlert } from "./alert-table";
55+
5856
const AssigneeLabel = ({ email }: { email: string }) => {
5957
const user = useUser(email);
6058
return user ? user.name : email;
@@ -169,6 +167,12 @@ export function AlertTableServerSide({
169167
pageSize: 20,
170168
});
171169

170+
const [viewedAlerts, setViewedAlerts] = useLocalStorage<ViewedAlert[]>(
171+
`viewed-alerts-${presetName}`,
172+
[]
173+
);
174+
const [lastViewedAlert, setLastViewedAlert] = useState<string | null>(null);
175+
172176
useEffect(() => {
173177
const filterArray = [];
174178

@@ -306,6 +310,22 @@ export function AlertTableServerSide({
306310
if (presetName === "alert-history") {
307311
return;
308312
}
313+
314+
// Update viewed alerts
315+
setViewedAlerts((prev) => {
316+
const newViewedAlerts = prev.filter(
317+
(a) => a.fingerprint !== alert.fingerprint
318+
);
319+
return [
320+
...newViewedAlerts,
321+
{
322+
fingerprint: alert.fingerprint,
323+
viewedAt: new Date().toISOString(),
324+
},
325+
];
326+
});
327+
328+
setLastViewedAlert(alert.fingerprint);
309329
setSelectedAlert(alert);
310330
setIsSidebarOpen(true);
311331
};
@@ -528,6 +548,8 @@ export function AlertTableServerSide({
528548
showFilterEmptyState={showFilterEmptyState}
529549
showSearchEmptyState={showSearchEmptyState}
530550
theme={theme}
551+
viewedAlerts={viewedAlerts}
552+
lastViewedAlert={lastViewedAlert}
531553
onRowClick={handleRowClick}
532554
presetName={presetName}
533555
/>

keep-ui/app/(keep)/alerts/alert-table.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ interface PresetTab {
3939
filter: string;
4040
id?: string;
4141
}
42+
43+
export interface ViewedAlert {
44+
fingerprint: string;
45+
viewedAt: string;
46+
}
47+
4248
interface Props {
4349
alerts: AlertDto[];
4450
columns: ColumnDef<AlertDto>[];
@@ -103,6 +109,12 @@ export function AlertTable({
103109
[]
104110
);
105111

112+
const [viewedAlerts, setViewedAlerts] = useLocalStorage<ViewedAlert[]>(
113+
`viewed-alerts-${presetName}`,
114+
[]
115+
);
116+
const [lastViewedAlert, setLastViewedAlert] = useState<string | null>(null);
117+
106118
const handleFacetDelete = (facetKey: string) => {
107119
setDynamicFacets((prevFacets) =>
108120
prevFacets.filter((df) => df.key !== facetKey)
@@ -268,10 +280,31 @@ export function AlertTable({
268280
if (presetName === "alert-history") {
269281
return;
270282
}
283+
284+
// Update viewed alerts
285+
setViewedAlerts((prev) => {
286+
const newViewedAlerts = prev.filter(
287+
(a) => a.fingerprint !== alert.fingerprint
288+
);
289+
return [
290+
...newViewedAlerts,
291+
{
292+
fingerprint: alert.fingerprint,
293+
viewedAt: new Date().toISOString(),
294+
},
295+
];
296+
});
297+
298+
setLastViewedAlert(alert.fingerprint);
271299
setSelectedAlert(alert);
272300
setIsSidebarOpen(true);
273301
};
274302

303+
// Reset last viewed alert when sidebar closes
304+
const handleSidebarClose = () => {
305+
setIsSidebarOpen(false);
306+
};
307+
275308
return (
276309
// Add h-screen to make it full height and remove the default flex-col gap
277310
<div className="h-screen flex flex-col gap-4">
@@ -355,6 +388,8 @@ export function AlertTable({
355388
theme={theme}
356389
onRowClick={handleRowClick}
357390
presetName={presetName}
391+
viewedAlerts={viewedAlerts}
392+
lastViewedAlert={lastViewedAlert}
358393
/>
359394
</Table>
360395
</div>
@@ -375,7 +410,7 @@ export function AlertTable({
375410

376411
<AlertSidebar
377412
isOpen={isSidebarOpen}
378-
toggle={() => setIsSidebarOpen(false)}
413+
toggle={handleSidebarClose}
379414
alert={selectedAlert}
380415
setRunWorkflowModalAlert={setRunWorkflowModalAlert}
381416
setDismissModalAlert={setDismissModalAlert}

keep-ui/app/(keep)/alerts/alerts-table-body.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import PushAlertToServerModal from "./alert-push-alert-to-server-modal";
88
import { EmptyStateCard } from "@/components/ui/EmptyStateCard";
99
import { MagnifyingGlassIcon, FunnelIcon } from "@heroicons/react/24/outline";
1010
import { GroupedRow } from "./alert-grouped-row";
11+
import { ViewedAlert } from "./alert-table";
1112

1213
interface Props {
1314
table: Table<AlertDto>;
@@ -18,6 +19,8 @@ interface Props {
1819
theme: { [key: string]: string };
1920
onRowClick: (alert: AlertDto) => void;
2021
presetName: string;
22+
viewedAlerts: ViewedAlert[];
23+
lastViewedAlert: string | null;
2124
}
2225

2326
export function AlertsTableBody({
@@ -29,6 +32,8 @@ export function AlertsTableBody({
2932
presetName,
3033
showFilterEmptyState,
3134
showSearchEmptyState,
35+
viewedAlerts,
36+
lastViewedAlert,
3237
}: Props) {
3338
const [modalOpen, setModalOpen] = useState(false);
3439

@@ -148,6 +153,8 @@ export function AlertsTableBody({
148153
table={table}
149154
theme={theme}
150155
onRowClick={handleRowClick}
156+
viewedAlerts={viewedAlerts}
157+
lastViewedAlert={lastViewedAlert}
151158
/>
152159
))}
153160
</TableBody>

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import Skeleton from "react-loading-skeleton";
1818
import "react-loading-skeleton/dist/skeleton.css";
1919
import { DynamicImageProviderIcon } from "@/components/ui";
2020

21+
// TODO: REFACTOR THIS TO SUPPORT ANY ACTIVITY TYPE, IT'S A MESS!
22+
2123
interface IncidentActivity {
2224
id: string;
23-
type: "comment" | "alert" | "newcomment";
25+
type: "comment" | "alert" | "newcomment" | "statuschange";
2426
text?: string;
2527
timestamp: string;
2628
initiator?: string | AlertDto;
@@ -84,7 +86,6 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
8486
_auditEventsLoading || (!auditEvents && !auditEventsError);
8587
const incidentEventsLoading =
8688
_incidentEventsLoading || (!incidentEvents && !incidentEventsError);
87-
const usersLoading = _usersLoading || (!users && !usersError);
8889

8990
const auditActivities = useMemo(() => {
9091
if (!auditEvents.length && !incidentEvents.length) {
@@ -101,6 +102,8 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
101102
const _type =
102103
auditEvent.action === "A comment was added to the incident" // @tb: I wish this was INCIDENT_COMMENT and not the text..
103104
? "comment"
105+
: auditEvent.action === "Incident status changed"
106+
? "statuschange"
104107
: "alert";
105108
return {
106109
id: auditEvent.id,
@@ -111,7 +114,10 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
111114
: alerts?.items.find(
112115
(a) => a.fingerprint === auditEvent.fingerprint
113116
),
114-
text: _type === "comment" ? auditEvent.description : "",
117+
text:
118+
_type === "comment" || _type === "statuschange"
119+
? auditEvent.description
120+
: "",
115121
timestamp: auditEvent.timestamp,
116122
} as IncidentActivity;
117123
}) || []
@@ -141,7 +147,7 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
141147
/>
142148
);
143149
} else {
144-
const source = (activity.initiator as AlertDto)?.source?.[0];
150+
const source = (activity.initiator as AlertDto)?.source?.[0] || "keep";
145151
const imagePath = `/icons/${source}-icon.png`;
146152
return (
147153
<DynamicImageProviderIcon

keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function IncidentActivityComment({
1919

2020
const onSubmit = useCallback(async () => {
2121
try {
22-
const response = await api.post(`/incidents/${incident.id}/comment`, {
22+
await api.post(`/incidents/${incident.id}/comment`, {
2323
status: incident.status,
2424
comment,
2525
});

keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityItem.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import AlertSeverity from "@/app/(keep)/alerts/alert-severity";
22
import { AlertDto } from "@/entities/alerts/model";
33
import TimeAgo from "react-timeago";
44

5+
// TODO: REFACTOR THIS TO SUPPORT ANY ACTIVITY TYPE, IT'S A MESS!
6+
57
export function IncidentActivityItem({ activity }: { activity: any }) {
68
const title =
79
typeof activity.initiator === "string"
810
? activity.initiator
911
: activity.initiator?.name;
1012
const subTitle =
11-
typeof activity.initiator === "string"
13+
activity.type === "comment"
1214
? " Added a comment. "
13-
: (activity.initiator?.status === "firing" ? " triggered" : " resolved") +
14-
". ";
15+
: activity.type === "statuschange"
16+
? " Incident status changed. "
17+
: activity.initiator?.status === "firing"
18+
? " triggered"
19+
: " resolved" + ". ";
1520
return (
1621
<div className="relative h-full w-full flex flex-col">
1722
<div className="flex items-center gap-2">

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ const AlertEventInfo: React.FC<{ event: AuditEvent; alert: AlertDto }> = ({
5959
}) => {
6060
return (
6161
<div className="h-full p-4 bg-gray-100 border-l">
62-
<h2 className="font-semibold mb-2">
63-
{alert.name} ({alert.fingerprint})
64-
</h2>
62+
<h2 className="font-semibold mb-2">{alert.name}</h2>
6563
<p className="mb-2 text-md">{alert.description}</p>
6664
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
6765
<p className="text-gray-400">Date:</p>

0 commit comments

Comments
 (0)