Skip to content

Commit 74513f5

Browse files
authored
fix: assignee missing alert table (#5292)
1 parent 76c396f commit 74513f5

File tree

3 files changed

+308
-3
lines changed

3 files changed

+308
-3
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import React from "react";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import AlertAssignee from "../alert-assignee";
4+
import { useUsers } from "@/entities/users/model/useUsers";
5+
import { User } from "@/app/(keep)/settings/models";
6+
7+
// Mock the useUsers hook
8+
jest.mock("@/entities/users/model/useUsers");
9+
10+
// Mock the NameInitialsAvatar component
11+
jest.mock("react-name-initials-avatar", () => ({
12+
NameInitialsAvatar: ({ name, bgColor, textColor, size }: any) => (
13+
<div
14+
data-testid="name-initials-avatar"
15+
data-name={name}
16+
data-bg-color={bgColor}
17+
data-text-color={textColor}
18+
data-size={size}
19+
>
20+
{name}
21+
</div>
22+
),
23+
}));
24+
25+
const mockUseUsers = useUsers as jest.MockedFunction<typeof useUsers>;
26+
27+
describe("AlertAssignee", () => {
28+
const createMockUser = (overrides: Partial<User> = {}): User => ({
29+
name: "John Doe",
30+
31+
role: "admin",
32+
picture: "https://example.com/avatar.jpg",
33+
created_at: "2023-01-01T00:00:00Z",
34+
last_login: "2023-12-01T00:00:00Z",
35+
ldap: false,
36+
groups: [],
37+
...overrides,
38+
});
39+
40+
beforeEach(() => {
41+
jest.clearAllMocks();
42+
});
43+
44+
it("should return null when no assignee is provided", () => {
45+
mockUseUsers.mockReturnValue({
46+
data: [],
47+
error: undefined,
48+
isLoading: false,
49+
isValidating: false,
50+
mutate: jest.fn(),
51+
});
52+
53+
const { container } = render(<AlertAssignee assignee={undefined} />);
54+
expect(container.firstChild).toBeNull();
55+
});
56+
57+
it("should return null when assignee is empty string", () => {
58+
mockUseUsers.mockReturnValue({
59+
data: [],
60+
error: undefined,
61+
isLoading: false,
62+
isValidating: false,
63+
mutate: jest.fn(),
64+
});
65+
66+
const { container } = render(<AlertAssignee assignee="" />);
67+
expect(container.firstChild).toBeNull();
68+
});
69+
70+
it("should show fallback UI when users haven't loaded yet", () => {
71+
mockUseUsers.mockReturnValue({
72+
data: [], // Empty array simulates loading state
73+
error: undefined,
74+
isLoading: true,
75+
isValidating: false,
76+
mutate: jest.fn(),
77+
});
78+
79+
render(<AlertAssignee assignee="[email protected]" />);
80+
81+
// Should show the first letter of the email in uppercase
82+
expect(screen.getByText("T")).toBeInTheDocument();
83+
84+
// Should show the full email as text
85+
expect(screen.getByText("[email protected]")).toBeInTheDocument();
86+
87+
// Should have the correct title attribute
88+
expect(screen.getByTitle("[email protected]")).toBeInTheDocument();
89+
});
90+
91+
it("should display user image when user is found and has picture", () => {
92+
const mockUser = createMockUser({
93+
94+
name: "John Doe",
95+
picture: "https://example.com/john.jpg",
96+
});
97+
98+
mockUseUsers.mockReturnValue({
99+
data: [mockUser],
100+
error: undefined,
101+
isLoading: false,
102+
isValidating: false,
103+
mutate: jest.fn(),
104+
});
105+
106+
render(<AlertAssignee assignee="[email protected]" />);
107+
108+
const image = screen.getByRole("img");
109+
expect(image).toHaveAttribute("src", "https://example.com/john.jpg");
110+
expect(image).toHaveAttribute("alt", "[email protected] profile picture");
111+
expect(image).toHaveAttribute("title", "[email protected]");
112+
});
113+
114+
it("should use generated avatar URL when user is found but has no picture", () => {
115+
const mockUser = createMockUser({
116+
117+
name: "Jane Doe",
118+
picture: undefined,
119+
});
120+
121+
mockUseUsers.mockReturnValue({
122+
data: [mockUser],
123+
error: undefined,
124+
isLoading: false,
125+
isValidating: false,
126+
mutate: jest.fn(),
127+
});
128+
129+
render(<AlertAssignee assignee="[email protected]" />);
130+
131+
const image = screen.getByRole("img");
132+
expect(image).toHaveAttribute(
133+
"src",
134+
"https://ui-avatars.com/api/?name=Jane Doe&background=random"
135+
);
136+
});
137+
138+
it("should use assignee email as fallback when user is not found", () => {
139+
mockUseUsers.mockReturnValue({
140+
data: [createMockUser({ email: "[email protected]" })],
141+
error: undefined,
142+
isLoading: false,
143+
isValidating: false,
144+
mutate: jest.fn(),
145+
});
146+
147+
render(<AlertAssignee assignee="[email protected]" />);
148+
149+
const image = screen.getByRole("img");
150+
expect(image).toHaveAttribute(
151+
"src",
152+
"https://ui-avatars.com/api/[email protected]&background=random"
153+
);
154+
});
155+
156+
it("should fall back to NameInitialsAvatar on image load error", () => {
157+
const mockUser = createMockUser({
158+
159+
name: "John Doe",
160+
picture: "https://example.com/broken-image.jpg",
161+
});
162+
163+
mockUseUsers.mockReturnValue({
164+
data: [mockUser],
165+
error: undefined,
166+
isLoading: false,
167+
isValidating: false,
168+
mutate: jest.fn(),
169+
});
170+
171+
render(<AlertAssignee assignee="[email protected]" />);
172+
173+
const image = screen.getByRole("img");
174+
175+
// Simulate image load error
176+
fireEvent.error(image);
177+
178+
// Should now show the NameInitialsAvatar component
179+
expect(screen.getByTestId("name-initials-avatar")).toBeInTheDocument();
180+
expect(screen.getByTestId("name-initials-avatar")).toHaveAttribute(
181+
"data-name",
182+
"John Doe"
183+
);
184+
expect(screen.getByTestId("name-initials-avatar")).toHaveAttribute(
185+
"data-bg-color",
186+
"orange"
187+
);
188+
expect(screen.getByTestId("name-initials-avatar")).toHaveAttribute(
189+
"data-text-color",
190+
"white"
191+
);
192+
expect(screen.getByTestId("name-initials-avatar")).toHaveAttribute(
193+
"data-size",
194+
"32px"
195+
);
196+
197+
// Original image should no longer be in the document
198+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
199+
});
200+
201+
it("should use user name for NameInitialsAvatar when image fails and user is found", () => {
202+
const mockUser = createMockUser({
203+
204+
name: "John Doe",
205+
});
206+
207+
mockUseUsers.mockReturnValue({
208+
data: [mockUser],
209+
error: undefined,
210+
isLoading: false,
211+
isValidating: false,
212+
mutate: jest.fn(),
213+
});
214+
215+
render(<AlertAssignee assignee="[email protected]" />);
216+
217+
const image = screen.getByRole("img");
218+
fireEvent.error(image);
219+
220+
const avatar = screen.getByTestId("name-initials-avatar");
221+
expect(avatar).toHaveAttribute("data-name", "John Doe");
222+
});
223+
224+
it("should use assignee email for NameInitialsAvatar when image fails and user is not found", () => {
225+
// Need to have at least one user in the array to trigger image rendering instead of fallback
226+
const otherUser = createMockUser({ email: "[email protected]" });
227+
228+
mockUseUsers.mockReturnValue({
229+
data: [otherUser], // User exists but not the one we're looking for
230+
error: undefined,
231+
isLoading: false,
232+
isValidating: false,
233+
mutate: jest.fn(),
234+
});
235+
236+
render(<AlertAssignee assignee="[email protected]" />);
237+
238+
const image = screen.getByRole("img");
239+
fireEvent.error(image);
240+
241+
const avatar = screen.getByTestId("name-initials-avatar");
242+
expect(avatar).toHaveAttribute("data-name", "[email protected]");
243+
});
244+
245+
it("should handle users array with multiple users correctly", () => {
246+
const users = [
247+
createMockUser({ email: "[email protected]", name: "User One", picture: undefined }),
248+
createMockUser({ email: "[email protected]", name: "User Two", picture: undefined }),
249+
createMockUser({ email: "[email protected]", name: "User Three", picture: undefined }),
250+
];
251+
252+
mockUseUsers.mockReturnValue({
253+
data: users,
254+
error: undefined,
255+
isLoading: false,
256+
isValidating: false,
257+
mutate: jest.fn(),
258+
});
259+
260+
render(<AlertAssignee assignee="[email protected]" />);
261+
262+
const image = screen.getByRole("img");
263+
expect(image).toHaveAttribute(
264+
"src",
265+
"https://ui-avatars.com/api/?name=User Two&background=random"
266+
);
267+
});
268+
269+
it("should handle special characters in assignee email", () => {
270+
// Need to have at least one user to avoid fallback UI
271+
const otherUser = createMockUser({ email: "[email protected]", picture: undefined });
272+
273+
mockUseUsers.mockReturnValue({
274+
data: [otherUser],
275+
error: undefined,
276+
isLoading: false,
277+
isValidating: false,
278+
mutate: jest.fn(),
279+
});
280+
281+
render(<AlertAssignee assignee="[email protected]" />);
282+
283+
const image = screen.getByRole("img");
284+
expect(image).toHaveAttribute(
285+
"src",
286+
"https://ui-avatars.com/api/[email protected]&background=random"
287+
);
288+
});
289+
});

keep-ui/widgets/alerts-table/ui/alert-assignee.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,28 @@ export default function AlertAssignee({ assignee }: Props) {
1010
const [imageError, setImageError] = useState(false);
1111
const { data: users = [] } = useUsers();
1212

13-
if (!assignee || users.length < 1) {
13+
if (!assignee) {
1414
return null;
1515
}
1616

1717
const user = users.find((user) => user.email === assignee);
18-
const userName = user?.name || "Keep";
18+
const userName = user?.name || assignee;
19+
20+
// If users haven't loaded yet, show a simple text fallback
21+
if (users.length === 0) {
22+
return (
23+
<div className="flex items-center gap-2">
24+
<div className="h-8 w-8 rounded-full bg-orange-100 flex items-center justify-center">
25+
<span className="text-xs font-medium text-orange-600">
26+
{assignee.charAt(0).toUpperCase()}
27+
</span>
28+
</div>
29+
<span className="text-sm text-gray-700 truncate" title={assignee}>
30+
{assignee}
31+
</span>
32+
</div>
33+
);
34+
}
1935

2036
return !imageError ? (
2137
// eslint-disable-next-line @next/next/no-img-element

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "keep"
3-
version = "0.47.7"
3+
version = "0.47.8"
44
description = "Alerting. for developers, by developers."
55
authors = ["Keep Alerting LTD"]
66
packages = [{include = "keep"}]

0 commit comments

Comments
 (0)