Skip to content

Commit 86a9bdf

Browse files
Replacing NotificationsStore with custom hooks. (#22741)
* Explicitly type response of notifications endpoint. * Replacing `NotificationsStore` with custom hooks. * Mocking `NotificationBadge` in test. * Disabling linter hint. * Fixing backend API call when key is not present.
1 parent a9d80b8 commit 86a9bdf

File tree

19 files changed

+259
-352
lines changed

19 files changed

+259
-352
lines changed

graylog2-server/src/main/java/org/graylog2/notifications/Notification.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@
1616
*/
1717
package org.graylog2.notifications;
1818

19+
import com.fasterxml.jackson.annotation.JsonValue;
1920
import org.graylog2.cluster.Node;
2021
import org.graylog2.plugin.database.Persisted;
2122
import org.joda.time.DateTime;
2223

2324
import javax.annotation.Nullable;
25+
import java.util.Locale;
2426
import java.util.Map;
2527

2628
public interface Notification extends Persisted {
27-
// Some pre-defined detail keys
28-
final String KEY_TITLE = "title";
29-
final String KEY_DESCRIPTION = "description";
30-
3129
Notification addType(Type type);
3230

3331
Notification addKey(String key);
@@ -100,10 +98,20 @@ enum Type {
10098
REMOTE_REINDEX_FINISHED,
10199
DATA_NODE_VERSION_MISMATCH,
102100
DATA_TIERING_ROLLOVER_ERROR,
103-
DATA_NODE_HEAP_WARNING
101+
DATA_NODE_HEAP_WARNING;
102+
103+
@JsonValue
104+
public String json() {
105+
return this.name().toLowerCase(Locale.ROOT);
106+
}
104107
}
105108

106109
enum Severity {
107-
NORMAL, URGENT
110+
NORMAL, URGENT;
111+
112+
@JsonValue
113+
public String json() {
114+
return this.name().toLowerCase(Locale.ROOT);
115+
}
108116
}
109117
}

graylog2-server/src/main/java/org/graylog2/notifications/NotificationImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.graylog2.notifications;
1818

19+
import com.fasterxml.jackson.annotation.JsonIgnore;
1920
import com.google.common.collect.Maps;
2021
import org.bson.types.ObjectId;
2122
import org.graylog2.cluster.Node;
@@ -177,6 +178,7 @@ public Notification addNode(String nodeId) {
177178
}
178179

179180
@Override
181+
@JsonIgnore
180182
public Map<String, Validator> getValidations() {
181183
return Collections.emptyMap();
182184
}

graylog2-server/src/main/java/org/graylog2/rest/resources/system/NotificationsResource.java

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@
1717
package org.graylog2.rest.resources.system;
1818

1919
import com.codahale.metrics.annotation.Timed;
20-
import com.google.common.collect.ImmutableMap;
21-
import com.google.common.collect.Lists;
2220
import io.swagger.annotations.Api;
2321
import io.swagger.annotations.ApiOperation;
2422
import io.swagger.annotations.ApiParam;
2523
import io.swagger.annotations.ApiResponse;
2624
import io.swagger.annotations.ApiResponses;
25+
import jakarta.inject.Inject;
26+
import jakarta.ws.rs.BadRequestException;
27+
import jakarta.ws.rs.DELETE;
28+
import jakarta.ws.rs.GET;
29+
import jakarta.ws.rs.Path;
30+
import jakarta.ws.rs.PathParam;
31+
import jakarta.ws.rs.Produces;
32+
import jakarta.ws.rs.core.MediaType;
2733
import org.apache.shiro.authz.annotation.RequiresAuthentication;
2834
import org.graylog2.audit.AuditEventTypes;
2935
import org.graylog2.audit.jersey.AuditEvent;
@@ -35,20 +41,8 @@
3541
import org.slf4j.LoggerFactory;
3642

3743
import javax.annotation.Nullable;
38-
39-
import jakarta.inject.Inject;
40-
41-
import jakarta.ws.rs.BadRequestException;
42-
import jakarta.ws.rs.DELETE;
43-
import jakarta.ws.rs.GET;
44-
import jakarta.ws.rs.Path;
45-
import jakarta.ws.rs.PathParam;
46-
import jakarta.ws.rs.Produces;
47-
import jakarta.ws.rs.core.MediaType;
48-
4944
import java.util.List;
5045
import java.util.Locale;
51-
import java.util.Map;
5246

5347
import static org.graylog2.shared.rest.documentation.generator.Generator.CLOUD_VISIBLE;
5448

@@ -66,29 +60,22 @@ public NotificationsResource(NotificationService notificationService) {
6660
this.notificationService = notificationService;
6761
}
6862

63+
public record NotificationsResponse(int total, List<Notification> notifications) {
64+
static NotificationsResponse create(List<Notification> notifications) {
65+
return new NotificationsResponse(notifications.size(), notifications);
66+
}
67+
}
68+
6969
@GET
7070
@Timed
7171
@ApiOperation(value = "Get all active notifications")
7272
@Produces(MediaType.APPLICATION_JSON)
73-
public Map<String, Object> listNotifications() {
74-
final List<Map<String, Object>> notifications = Lists.newArrayList();
75-
for (Notification n : notificationService.all()) {
76-
final String notificationType = n.getType().toString().toLowerCase(Locale.ENGLISH);
77-
if (!isPermitted(RestPermissions.NOTIFICATIONS_READ, notificationType)) {
78-
continue;
79-
}
80-
81-
final Map<String, Object> notification = n.asMap();
82-
try {
83-
notifications.add(notification);
84-
} catch (IllegalArgumentException e) {
85-
LOG.warn("There is a notification type we can't handle: [" + n.getType() + "]");
86-
}
87-
}
73+
public NotificationsResponse listNotifications() {
74+
final var notifications = notificationService.all().stream()
75+
.filter(notification -> isPermitted(RestPermissions.NOTIFICATIONS_READ, notification.getType().toString()))
76+
.toList();
8877

89-
return ImmutableMap.of(
90-
"total", notifications.size(),
91-
"notifications", notifications);
78+
return NotificationsResponse.create(notifications);
9279
}
9380

9481
@DELETE

graylog2-web-interface/src/components/navigation/NotificationBadge.test.tsx

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,53 +15,41 @@
1515
* <http://www.mongodb.com/licensing/server-side-public-license>.
1616
*/
1717
import * as React from 'react';
18-
import { render, screen, waitFor, act, within } from 'wrappedTestingLibrary';
18+
import { render, screen, waitFor, within } from 'wrappedTestingLibrary';
1919

20-
import { MockStore, asMock } from 'helpers/mocking';
21-
import { NotificationsActions, NotificationsStore } from 'stores/notifications/NotificationsStore';
20+
import { asMock } from 'helpers/mocking';
21+
import useNotifications from 'components/notifications/useNotifications';
2222

2323
import NotificationBadge from './NotificationBadge';
2424

25-
jest.mock('stores/notifications/NotificationsStore', () => ({
26-
NotificationsActions: { list: jest.fn() },
27-
NotificationsStore: MockStore(),
28-
}));
29-
3025
const BADGE_ID = 'notification-badge';
3126

32-
type NotificationsStoreType = ReturnType<typeof NotificationsStore.getInitialState>;
33-
34-
const setNotificationCount = (count: number) => {
35-
asMock(NotificationsStore.getInitialState).mockReturnValue({ total: count } as NotificationsStoreType);
36-
};
37-
38-
describe('NotificationBadge', () => {
39-
beforeAll(() => {
40-
jest.useFakeTimers();
41-
});
42-
43-
afterAll(() => {
44-
jest.useRealTimers();
45-
});
46-
47-
beforeEach(() => {
48-
jest.restoreAllMocks();
49-
jest.clearAllMocks();
50-
});
51-
52-
it('triggers update of notifications', async () => {
53-
expect(NotificationsActions.list).not.toHaveBeenCalled();
54-
55-
render(<NotificationBadge />);
56-
57-
jest.advanceTimersByTime(3000);
58-
await waitFor(() => {
59-
expect(NotificationsActions.list).toHaveBeenCalled();
60-
});
27+
jest.mock('components/notifications/useNotifications');
28+
29+
const notificationFixture = {
30+
id: 'deadbeef',
31+
details: {},
32+
validations: {},
33+
fields: {},
34+
severity: 'urgent',
35+
type: 'no_input_running',
36+
key: 'test',
37+
timestamp: '2022-12-12T10:55:55.014Z',
38+
node_id: '3fcc3889-18a3-4a0d-821c-0fd560d152e7',
39+
} as const;
40+
41+
const createNotifications = (count: number) => new Array(count).fill(notificationFixture);
42+
43+
const setNotificationCount = (count: number) =>
44+
asMock(useNotifications).mockReturnValue({
45+
data: { total: count, notifications: createNotifications(count) },
46+
isLoading: false,
6147
});
6248

49+
describe('NotificationBadge', () => {
6350
it('renders nothing when there are no notifications', () => {
6451
setNotificationCount(0);
52+
6553
render(<NotificationBadge />);
6654

6755
expect(screen.queryByTestId(BADGE_ID)).not.toBeInTheDocument();
@@ -81,17 +69,15 @@ describe('NotificationBadge', () => {
8169
it('updates notification count when triggered by store', async () => {
8270
setNotificationCount(42);
8371

84-
render(<NotificationBadge />);
72+
const { rerender } = render(<NotificationBadge />);
8573

8674
const badgeBefore = await screen.findByTestId(BADGE_ID);
8775

8876
expect(within(badgeBefore).getByText(42)).toBeInTheDocument();
8977

90-
const cb = asMock(NotificationsStore.listen).mock.calls[0][0];
78+
setNotificationCount(23);
9179

92-
act(() => {
93-
cb({ total: 23 } as NotificationsStoreType);
94-
});
80+
rerender(<NotificationBadge />);
9581

9682
const badgeAfter = await screen.findByTestId(BADGE_ID);
9783

graylog2-web-interface/src/components/navigation/NotificationBadge.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
* <http://www.mongodb.com/licensing/server-side-public-license>.
1616
*/
1717
import * as React from 'react';
18-
import { useEffect } from 'react';
1918
import styled from 'styled-components';
2019

2120
import { LinkContainer } from 'components/common/router';
2221
import { Badge, Nav } from 'components/bootstrap';
23-
import { useStore } from 'stores/connect';
2422
import Routes from 'routing/Routes';
25-
import { NotificationsActions, NotificationsStore } from 'stores/notifications/NotificationsStore';
2623
import { NAV_ITEM_HEIGHT } from 'theme/constants';
24+
import useNotifications from 'components/notifications/useNotifications';
2725

2826
import InactiveNavItem from './InactiveNavItem';
2927

@@ -44,30 +42,20 @@ const StyledInactiveNavItem = styled(InactiveNavItem)`
4442
}
4543
`;
4644

47-
const POLL_INTERVAL = 3000;
48-
4945
const NotificationBadge = () => {
50-
const total = useStore(NotificationsStore, (notifications) => notifications?.total);
51-
52-
useEffect(() => {
53-
const interval = setInterval(NotificationsActions.list, POLL_INTERVAL);
54-
55-
return () => {
56-
clearInterval(interval);
57-
};
58-
}, []);
46+
const { data, isLoading } = useNotifications();
5947

60-
return total ? (
48+
return isLoading || data.total === 0 ? null : (
6149
<StyledNav navbar>
6250
<LinkContainer to={Routes.SYSTEM.OVERVIEW}>
6351
<StyledInactiveNavItem>
6452
<Badge bsStyle="danger" data-testid="notification-badge" title="System Notifications">
65-
{total}
53+
{data.total}
6654
</Badge>
6755
</StyledInactiveNavItem>
6856
</LinkContainer>
6957
</StyledNav>
70-
) : null;
58+
);
7159
};
7260

7361
export default NotificationBadge;

graylog2-web-interface/src/components/notifications/Notification.test.tsx

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515
* <http://www.mongodb.com/licensing/server-side-public-license>.
1616
*/
1717
import * as React from 'react';
18-
import { render, act, screen, waitFor } from 'wrappedTestingLibrary';
18+
import { render, screen } from 'wrappedTestingLibrary';
1919

20-
import MockStore from 'helpers/mocking/StoreMock';
2120
import asMock from 'helpers/mocking/AsMock';
22-
import NotificationsFactory from 'logic/notifications/NotificationsFactory';
23-
import { NotificationsActions, NotificationsStore } from 'stores/notifications/NotificationsStore';
21+
import useNotificationMessage from 'components/notifications/useNotificationMessage';
2422

2523
import Notification from './Notification';
2624

@@ -30,65 +28,29 @@ const notificationMessageFixture = {
3028
'\n<span>\nThere is a node without any running inputs. This means that you are not receiving any messages from this\nnode at this point in time. This is most probably an indication of an error or misconfiguration.\n You can click <a href="/system/inputs" target="_blank" rel="noreferrer">here</a> to solve this.\n</span>\n',
3129
};
3230
const notificationFixture = {
31+
id: 'deadbeef',
32+
details: {},
33+
validations: {},
34+
fields: {},
3335
severity: 'urgent',
3436
type: 'no_input_running',
3537
key: 'test',
3638
timestamp: '2022-12-12T10:55:55.014Z',
3739
node_id: '3fcc3889-18a3-4a0d-821c-0fd560d152e7',
38-
};
40+
} as const;
3941

40-
jest.mock('stores/notifications/NotificationsStore', () => ({
41-
NotificationsStore: MockStore([
42-
'getInitialState',
43-
jest.fn(() => ({
44-
messages: {},
45-
})),
46-
]),
47-
NotificationsActions: {
48-
getInitialState: jest.fn(),
49-
getHtmlMessage: jest.fn(() => Promise.resolve(notificationMessageFixture)),
50-
},
51-
}));
42+
jest.mock('components/notifications/useNotificationMessage');
5243

5344
describe('<Notification>', () => {
54-
afterEach(() => {
55-
jest.clearAllMocks();
56-
jest.useRealTimers();
57-
});
58-
59-
test('should render Notification', async () => {
60-
render(<Notification notification={notificationFixture} />);
61-
62-
await screen.findByText(/loading\.\.\./i);
63-
64-
await waitFor(() =>
65-
expect(NotificationsActions.getHtmlMessage).toHaveBeenCalledWith(
66-
notificationFixture.type,
67-
notificationFixture.key,
68-
NotificationsFactory.getValuesForNotification(notificationFixture),
69-
),
70-
);
45+
beforeEach(() => {
46+
asMock(useNotificationMessage).mockReturnValue(notificationMessageFixture);
7147
});
7248

73-
test('should render notification message', async () => {
74-
jest.useFakeTimers();
75-
76-
asMock(NotificationsStore.getInitialState).mockReturnValue({
77-
messages: { [`${notificationFixture.type}-${notificationFixture.key}`]: notificationMessageFixture },
78-
total: 1,
79-
notifications: [notificationFixture],
80-
});
81-
49+
test('it render notification message', async () => {
8250
render(<Notification notification={notificationFixture} />);
8351

84-
act(() => {
85-
jest.advanceTimersByTime(100);
86-
});
87-
88-
const alert = screen.getByRole('alert', {
52+
await screen.findByRole('alert', {
8953
name: /there is a node without any running inputs/i,
9054
});
91-
92-
await waitFor(() => expect(alert).toBeInTheDocument());
9355
});
9456
});

0 commit comments

Comments
 (0)