Skip to content

Commit 22ddd74

Browse files
committed
chore: enhance MessagesPage to manage URL parameters and prevent stale selections
1 parent d33d945 commit 22ddd74

3 files changed

Lines changed: 62 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Production breaks at 2 AM. Your cloud portal shows **5,000 messages in the Dead-
2828
> **Your cloud console shows you counts. ServiceHub shows you answers.**
2929
3030
> [!IMPORTANT]
31-
> **Built for strict environments.** Read-only by default (`Peek`, never consume) · connection strings AES-GCM-256 encrypted at rest · **zero external calls** — pattern analysis runs entirely in your browser, no message data ever leaves your network · destructive actions (replay, send) blocked on production namespaces. Details in [Security](#security).
31+
> **Built for strict environments.** Read-only by default (`Peek`, never consume) · connection strings AES-GCM-256 encrypted at rest · **zero external calls by default** — pattern analysis runs entirely in your browser and no message data ever leaves your network ([telemetry](#application-insights-telemetry) is opt-in and disabled unless you enable it) · destructive actions (replay, send) blocked on production namespaces. Details in [Security](#security).
3232
3333
> [!TIP]
3434
> **No credentials?** Try the built-in [Simulator Mode](#simulator-mode) — runs 3 synthetic namespaces (Azure + AWS + GCP) with 50 seeded messages each. No cloud account needed.

apps/web/src/__tests__/pages/MessagesPage.test.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { render, screen, fireEvent } from '@testing-library/react';
3-
import { MemoryRouter } from 'react-router-dom';
3+
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
44
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
55
import { MessagesPage } from '@/pages/MessagesPage';
66

@@ -89,13 +89,32 @@ const mockMessagesData = {
8989
totalCount: 2,
9090
};
9191

92+
// Exposes the live router URL to assertions, and a button that simulates the
93+
// app's sidebar navigation to another queue while (realistically) carrying the
94+
// existing query params along — including a stale ?message= deep link.
95+
function LocationProbe() {
96+
const location = useLocation();
97+
const navigate = useNavigate();
98+
return (
99+
<>
100+
<span data-testid="location-search">{location.search}</span>
101+
<button onClick={() => navigate('/messages?namespace=ns1&queue=other-queue&message=msg-1')}>
102+
go-other-queue
103+
</button>
104+
</>
105+
);
106+
}
107+
92108
function createWrapper(initialPath = '/messages?namespace=ns1&queue=test-queue') {
93109
const queryClient = new QueryClient({
94110
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
95111
});
96112
return ({ children }: { children: React.ReactNode }) => (
97113
<MemoryRouter initialEntries={[initialPath]}>
98-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
114+
<QueryClientProvider client={queryClient}>
115+
{children}
116+
<LocationProbe />
117+
</QueryClientProvider>
99118
</MemoryRouter>
100119
);
101120
}
@@ -288,12 +307,39 @@ describe('MessagesPage', () => {
288307
expect(screen.getByTestId('selected-id')).toHaveTextContent('msg-2');
289308
});
290309

310+
it('mirrors the selection into the URL so the link is shareable', () => {
311+
const Wrapper = createWrapper();
312+
render(<Wrapper><MessagesPage /></Wrapper>);
313+
expect(screen.getByTestId('location-search')).not.toHaveTextContent('message=');
314+
fireEvent.click(screen.getByText('Select msg-2'));
315+
expect(screen.getByTestId('location-search')).toHaveTextContent('message=msg-2');
316+
});
317+
291318
it('clears the selection when switching queue tabs', () => {
292319
const Wrapper = createWrapper('/messages?namespace=ns1&queue=test-queue&message=msg-1');
293320
render(<Wrapper><MessagesPage /></Wrapper>);
294321
expect(screen.getByTestId('selected-id')).toHaveTextContent('msg-1');
295322
fireEvent.click(screen.getByText(/Dead-Letter \(/));
296323
expect(screen.getByTestId('selected-id')).toHaveTextContent('none');
297324
});
325+
326+
it('removes the message param from the URL when switching queue tabs', () => {
327+
const Wrapper = createWrapper('/messages?namespace=ns1&queue=test-queue&message=msg-1');
328+
render(<Wrapper><MessagesPage /></Wrapper>);
329+
expect(screen.getByTestId('location-search')).toHaveTextContent('message=msg-1');
330+
fireEvent.click(screen.getByText(/Dead-Letter \(/));
331+
expect(screen.getByTestId('location-search')).not.toHaveTextContent('message=');
332+
});
333+
334+
it('clears selection and the stale message param when navigating to another entity', () => {
335+
const Wrapper = createWrapper('/messages?namespace=ns1&queue=test-queue&message=msg-1');
336+
render(<Wrapper><MessagesPage /></Wrapper>);
337+
expect(screen.getByTestId('selected-id')).toHaveTextContent('msg-1');
338+
// Sidebar-style navigation to a different queue carrying the old param along
339+
fireEvent.click(screen.getByText('go-other-queue'));
340+
expect(screen.getByTestId('selected-id')).toHaveTextContent('none');
341+
expect(screen.getByTestId('location-search')).toHaveTextContent('queue=other-queue');
342+
expect(screen.getByTestId('location-search')).not.toHaveTextContent('message=');
343+
});
298344
});
299345
});

apps/web/src/pages/MessagesPage.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,15 +179,26 @@ export function MessagesPage() {
179179
}
180180
}, [queueTypeParam]);
181181

182-
// Clear selection when switching queues/topics to prevent stale detail panel.
183-
// Skipped on first render so a deep-linked "?message=<id>" selection survives page load.
182+
// Clear selection when switching queues/topics to prevent stale detail panel,
183+
// and drop the ?message= param so the URL never carries a deep link into an
184+
// entity the message doesn't belong to. Skipped on first render so a
185+
// deep-linked "?message=<id>" selection survives page load.
184186
const isFirstEntityRender = useRef(true);
185187
useEffect(() => {
186188
if (isFirstEntityRender.current) {
187189
isFirstEntityRender.current = false;
188190
return;
189191
}
190192
setSelectedMessageId(null);
193+
setSearchParams(prev => {
194+
const next = new URLSearchParams(prev);
195+
next.delete('message');
196+
return next;
197+
}, { replace: true });
198+
// setSearchParams is deliberately omitted from deps: react-router recreates it on
199+
// every location change, so including it re-runs this effect after each URL update
200+
// and wipes a selection the instant it is made.
201+
// eslint-disable-next-line react-hooks/exhaustive-deps
191202
}, [namespaceId, queueName, topicName, subscriptionName]);
192203

193204
// Reset pagination when queue/topic/tab changes

0 commit comments

Comments
 (0)