Skip to content

Commit 7c5b4c8

Browse files
hemangsktalborenskynetigorKiryous
authored
feat: Add user mention functionality to incident comments (#4649)
Co-authored-by: Tal <tal@keephq.dev> Co-authored-by: Ihor Panasiuk <igorskynet13@gmail.com> Co-authored-by: Kirill Chernakov <yakiryous@gmail.com>
1 parent b5d3713 commit 7c5b4c8

17 files changed

+581
-70
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { AlertDto } from "@/entities/alerts/model";
3+
import { AlertDto, CommentMentionDto } from "@/entities/alerts/model";
44
import { IncidentDto } from "@/entities/incidents/model";
55
import { useUsers } from "@/entities/users/model/useUsers";
66
import UserAvatar from "@/components/navbar/UserAvatar";
@@ -20,12 +20,13 @@ import { DynamicImageProviderIcon } from "@/components/ui";
2020

2121
// TODO: REFACTOR THIS TO SUPPORT ANY ACTIVITY TYPE, IT'S A MESS!
2222

23-
interface IncidentActivity {
23+
export interface IncidentActivity {
2424
id: string;
2525
type: "comment" | "alert" | "newcomment" | "statuschange" | "assign";
2626
text?: string;
2727
timestamp: string;
2828
initiator?: string | AlertDto;
29+
mentions?: CommentMentionDto[];
2930
}
3031

3132
const ACTION_TYPES = [
@@ -58,7 +59,7 @@ function Item({
5859
{icon}
5960
</div>
6061
</div>
61-
<div className="py-6 flex-1">{children}</div>
62+
<div className="py-6 flex-1 min-w-0">{children}</div>
6263
</div>
6364
);
6465
}
@@ -139,6 +140,7 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
139140
? auditEvent.description
140141
: "",
141142
timestamp: auditEvent.timestamp,
143+
mentions: auditEvent.mentions,
142144
} as IncidentActivity;
143145
}) || []
144146
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Extracts tagged user IDs from Quill editor content
3+
* This is called when a comment is submitted to get the final list of mentions
4+
*
5+
* @param content - HTML content from the Quill editor
6+
* @returns Array of user IDs that were mentioned in the content
7+
*/
8+
export function extractTaggedUsers(content: string): string[] {
9+
const mentionRegex = /data-id="([^"]+)"/g;
10+
const ids = Array.from(content.matchAll(mentionRegex)).map(match => match[1]) || [];
11+
return ids;
12+
}
Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { IncidentDto } from "@/entities/incidents/model";
2-
import { TextInput, Button } from "@tremor/react";
3-
import { useState, useCallback, useEffect } from "react";
2+
import { Button } from "@tremor/react";
3+
import { useState, useCallback } from "react";
44
import { toast } from "react-toastify";
55
import { KeyedMutator } from "swr";
66
import { useApi } from "@/shared/lib/hooks/useApi";
77
import { showErrorToast } from "@/shared/ui";
88
import { AuditEvent } from "@/entities/alerts/model";
9+
import { useUsers } from "@/entities/users/model/useUsers";
10+
import { extractTaggedUsers } from "../lib/extractTaggedUsers";
11+
import { IncidentCommentInput } from "./IncidentCommentInput.dynamic";
912

13+
/**
14+
* Component for adding comments to an incident with user mention capability
15+
*/
1016
export function IncidentActivityComment({
1117
incident,
1218
mutator,
@@ -15,13 +21,17 @@ export function IncidentActivityComment({
1521
mutator: KeyedMutator<AuditEvent[]>;
1622
}) {
1723
const [comment, setComment] = useState("");
24+
1825
const api = useApi();
1926

27+
const { data: users = [] } = useUsers();
2028
const onSubmit = useCallback(async () => {
2129
try {
30+
const extractedTaggedUsers = extractTaggedUsers(comment);
2231
await api.post(`/incidents/${incident.id}/comment`, {
2332
status: incident.status,
2433
comment,
34+
tagged_users: extractedTaggedUsers,
2535
});
2636
toast.success("Comment added!", { position: "top-right" });
2737
setComment("");
@@ -31,42 +41,26 @@ export function IncidentActivityComment({
3141
}
3242
}, [api, incident.id, incident.status, comment, mutator]);
3343

34-
const handleKeyDown = useCallback(
35-
(event: KeyboardEvent) => {
36-
if (
37-
event.key === "Enter" &&
38-
(event.metaKey || event.ctrlKey) &&
39-
comment
40-
) {
41-
onSubmit();
42-
}
43-
},
44-
[onSubmit, comment]
45-
);
46-
47-
useEffect(() => {
48-
window.addEventListener("keydown", handleKeyDown);
49-
return () => {
50-
window.removeEventListener("keydown", handleKeyDown);
51-
};
52-
}, [comment, handleKeyDown]);
53-
5444
return (
55-
<div className="flex h-full w-full relative items-center">
56-
<TextInput
45+
<div className="border border-tremor-border rounded-tremor-default shadow-tremor-input flex flex-col">
46+
<IncidentCommentInput
5747
value={comment}
5848
onValueChange={setComment}
59-
placeholder="Add a new comment..."
49+
users={users}
50+
placeholder="Add a comment..."
51+
className="min-h-11"
6052
/>
61-
<Button
62-
color="orange"
63-
variant="secondary"
64-
className="ml-2.5"
65-
disabled={!comment}
66-
onClick={onSubmit}
67-
>
68-
Comment
69-
</Button>
53+
54+
<div className="flex justify-end p-2">
55+
<Button
56+
color="orange"
57+
variant="primary"
58+
disabled={!comment}
59+
onClick={onSubmit}
60+
>
61+
Comment
62+
</Button>
63+
</div>
7064
</div>
7165
);
7266
}

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
import { AlertSeverity } from "@/entities/alerts/ui";
22
import { AlertDto } from "@/entities/alerts/model";
33
import TimeAgo from "react-timeago";
4+
import { FormattedContent } from "@/shared/ui/FormattedContent/FormattedContent";
5+
import { IncidentActivity } from "../incident-activity";
46

57
// TODO: REFACTOR THIS TO SUPPORT ANY ACTIVITY TYPE, IT'S A MESS!
68

7-
export function IncidentActivityItem({ activity }: { activity: any }) {
9+
export function IncidentActivityItem({ activity }: { activity: IncidentActivity }) {
810
const title =
911
typeof activity.initiator === "string"
1012
? activity.initiator
11-
: activity.initiator?.name;
13+
: (activity.initiator as AlertDto)?.name;
1214
const subTitle =
1315
activity.type === "comment"
1416
? " Added a comment. "
1517
: activity.type === "statuschange"
1618
? " Incident status changed. "
17-
: activity.initiator?.status === "firing"
19+
: (activity.initiator as AlertDto)?.status === "firing"
1820
? " triggered"
1921
: " resolved" + ". ";
22+
23+
// Process comment text to style mentions if it's a comment with mentions
24+
const processCommentText = (text: string) => {
25+
if (!text || activity.type !== "comment") return text;
26+
27+
if (text.includes('<span class="mention">') || text.includes("<p>")) {
28+
return <FormattedContent format="html" content={text} />;
29+
}
30+
31+
return text;
32+
};
33+
2034
return (
2135
<div className="relative h-full w-full flex flex-col">
2236
<div className="flex items-center gap-2">
@@ -32,7 +46,9 @@ export function IncidentActivityItem({ activity }: { activity: any }) {
3246
</span>
3347
</div>
3448
{activity.text && (
35-
<div className="font-light text-gray-800">{activity.text}</div>
49+
<div className="font-light text-gray-800">
50+
{processCommentText(activity.text)}
51+
</div>
3652
)}
3753
</div>
3854
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import dynamic from "next/dynamic";
2+
3+
const IncidentCommentInput = dynamic(
4+
() =>
5+
import("./IncidentCommentInput").then((mod) => mod.IncidentCommentInput),
6+
{
7+
ssr: false,
8+
// mimic the quill editor while loading
9+
loading: () => (
10+
<div className="w-full h-11 px-[11px] py-[16px] leading-[1.42] text-tremor-default font-[Helvetica,Arial,sans-serif] text-[#0009] italic">
11+
Add a comment...
12+
</div>
13+
),
14+
}
15+
);
16+
17+
export { IncidentCommentInput };
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.incident-comment-input .ql-container {
2+
@apply text-tremor-default;
3+
}
4+
5+
.mention {
6+
background-color: #e8f4fe;
7+
border-radius: 4px;
8+
padding: 0 2px;
9+
color: #0366d6;
10+
}
11+
12+
.mention-container {
13+
display: block !important;
14+
position: absolute !important;
15+
background-color: white;
16+
border: 1px solid #ddd;
17+
border-radius: 4px;
18+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
19+
z-index: 9999 !important;
20+
max-height: 100%;
21+
overflow-y: auto;
22+
padding: 5px 0;
23+
min-width: 180px;
24+
}
25+
26+
.mention-list {
27+
list-style: none;
28+
margin: 0;
29+
padding: 0;
30+
}
31+
32+
.mention-item {
33+
display: block;
34+
padding: 8px 12px;
35+
cursor: pointer;
36+
color: #333;
37+
}
38+
39+
.mention-item:hover {
40+
background-color: #f0f0f0;
41+
}
42+
43+
.mention-item.selected {
44+
background-color: #e8f4fe;
45+
}
46+
47+
/* Prevent hidden overflow that could hide the dropdown */
48+
.ql-editor p {
49+
overflow: visible;
50+
}

0 commit comments

Comments
 (0)