Skip to content

feat: Add user mention functionality to incident comments #4649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

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

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

interface IncidentActivity {
export interface IncidentActivity {
id: string;
type: "comment" | "alert" | "newcomment" | "statuschange" | "assign";
text?: string;
timestamp: string;
initiator?: string | AlertDto;
mentions?: CommentMentionDto[];
}

const ACTION_TYPES = [
Expand Down Expand Up @@ -139,6 +140,7 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
? auditEvent.description
: "",
timestamp: auditEvent.timestamp,
mentions: auditEvent.mentions,
} as IncidentActivity;
}) || []
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { IncidentDto } from "@/entities/incidents/model";
import { TextInput, Button } from "@tremor/react";
import { useState, useCallback, useEffect } from "react";
import { Button } from "@tremor/react";
import { useState, useCallback } from "react";
import { toast } from "react-toastify";
import { KeyedMutator } from "swr";
import { useApi } from "@/shared/lib/hooks/useApi";
import { showErrorToast } from "@/shared/ui";
import { AuditEvent } from "@/entities/alerts/model";
import { useUsers } from "@/entities/users/model/useUsers";
import { IncidentCommentInput, extractTaggedUsers } from "./IncidentCommentInput";

/**
* Component for adding comments to an incident with user mention capability
*/
export function IncidentActivityComment({
incident,
mutator,
Expand All @@ -15,58 +20,54 @@ export function IncidentActivityComment({
mutator: KeyedMutator<AuditEvent[]>;
}) {
const [comment, setComment] = useState("");
const [taggedUsers, setTaggedUsers] = useState<string[]>([]);

const api = useApi();


const { data: users = [] } = useUsers();
const onSubmit = useCallback(async () => {
try {
const extractedTaggedUsers = extractTaggedUsers(comment);
console.log('Extracted tagged users:', extractedTaggedUsers);

await api.post(`/incidents/${incident.id}/comment`, {
status: incident.status,
comment,
tagged_users: extractedTaggedUsers,
});
toast.success("Comment added!", { position: "top-right" });
setComment("");
setTaggedUsers([]);
mutator();
} catch (error) {
showErrorToast(error, "Failed to add comment");
}
}, [api, incident.id, incident.status, comment, mutator]);

const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (
event.key === "Enter" &&
(event.metaKey || event.ctrlKey) &&
comment
) {
onSubmit();
}
},
[onSubmit, comment]
);

useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [comment, handleKeyDown]);

return (
<div className="flex h-full w-full relative items-center">
<TextInput
value={comment}
onValueChange={setComment}
placeholder="Add a new comment..."
/>
<Button
color="orange"
variant="secondary"
className="ml-2.5"
disabled={!comment}
onClick={onSubmit}
>
Comment
</Button>
<div className="relative border border-gray-300 rounded-md mb-4">
<div className="flex flex-col p-2.5 gap-2.5">
<div className="w-full">
<IncidentCommentInput
value={comment}
onValueChange={setComment}
users={users}
placeholder="Add a comment..."
className="comment-editor"
/>
</div>

<div className="flex justify-end mt-2">
<Button
color="orange"
variant="secondary"
disabled={!comment}
onClick={onSubmit}
>
Comment
</Button>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { AlertSeverity } from "@/entities/alerts/ui";
import { AlertDto } from "@/entities/alerts/model";
import { AlertDto, CommentMentionDto } from "@/entities/alerts/model";
import TimeAgo from "react-timeago";
import { useUsers } from "@/entities/users/model/useUsers";
import { User } from "@/app/(keep)/settings/models";

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

export function IncidentActivityItem({ activity }: { activity: any }) {
const { data: users = [] } = useUsers();

const title =
typeof activity.initiator === "string"
? activity.initiator
Expand All @@ -17,6 +21,33 @@ export function IncidentActivityItem({ activity }: { activity: any }) {
: activity.initiator?.status === "firing"
? " triggered"
: " resolved" + ". ";

// Process comment text to style mentions if it's a comment with mentions
const processCommentText = (text: string) => {
console.log(activity);
if (!text || activity.type !== 'comment') return text;

// Create a map of email to name for user lookup
const emailToName = new Map();
users.forEach((user: User) => {
if (user.email) {
emailToName.set(user.email, user.name || user.email);
}
});

// If the text contains HTML (from ReactQuill), it's already formatted
if (text.includes('<span class="mention">') || text.includes('<p>')) {
// Sanitize HTML to prevent XSS attacks if needed
// For a production app, consider using a library like DOMPurify

return (
<div className="quill-content" dangerouslySetInnerHTML={{ __html: text }} />
);
}

return text;
};

return (
<div className="relative h-full w-full flex flex-col">
<div className="flex items-center gap-2">
Expand All @@ -32,7 +63,9 @@ export function IncidentActivityItem({ activity }: { activity: any }) {
</span>
</div>
{activity.text && (
<div className="font-light text-gray-800">{activity.text}</div>
<div className="font-light text-gray-800">
{processCommentText(activity.text)}
</div>
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.quill-editor-container .quill {
display: block;
height: 100%;
}

.quill-editor-container .ql-container {
height: 100%;
font-size: 16px;
border: none;
border-top: 1px solid #ccc;
}

.quill-editor-container .ql-toolbar {
border: none;
border-bottom: 1px solid #eee;
}

.quill-editor-container .ql-editor {
min-height: 100%;
max-height: 100%;
overflow-y: auto;
}

.mention {
background-color: #E8F4FE;
border-radius: 4px;
padding: 0 2px;
color: #0366d6;
}

.mention-container {
display: block !important;
position: absolute !important;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
z-index: 9999 !important;
max-height: 100%;
overflow-y: auto;
padding: 5px 0;
min-width: 180px;
}

.mention-list {
list-style: none;
margin: 0;
padding: 0;
}

.mention-item {
display: block;
padding: 8px 12px;
cursor: pointer;
color: #333;
}

.mention-item:hover {
background-color: #f0f0f0;
}

.mention-item.selected {
background-color: #e8f4fe;
}

/* Prevent hidden overflow that could hide the dropdown */
.ql-editor p {
overflow: visible;
}
Loading
Loading