Skip to content

Commit 2901066

Browse files
authored
Merge pull request #143 from debdevops/fix/deep-dive-critical-issues
feat: Implement long-poll scanning for dead-letter messages in AwsMes…
2 parents 9fb8f64 + 83c6566 commit 2901066

51 files changed

Lines changed: 2884 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/src/__tests__/components/NamespaceCard.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ describe('NamespaceCard', () => {
121121
expect(errorText || hasSkeleton).toBeTruthy();
122122
});
123123

124-
it('Browse Queues button navigates to messages page', async () => {
124+
it('Browse Queues button navigates straight to the first queue', async () => {
125125
render(<NamespaceCard namespace={mockNamespace} />, { wrapper: createWrapper() });
126126
fireEvent.click(await screen.findByRole('button', { name: /browse queues/i }));
127-
expect(mockNavigate).toHaveBeenCalledWith('/messages?namespace=ns1');
127+
expect(mockNavigate).toHaveBeenCalledWith('/messages?namespace=ns1&queue=queue-1&queueType=active');
128128
});
129129

130130
it('View DLQ History button navigates to dlq-history page', async () => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,10 @@ describe('DashboardPage', () => {
175175
expect(await screen.findByText(/DLQ: 15 messages need attention/i)).toBeInTheDocument();
176176
});
177177

178-
it('Browse Queues button navigates to messages page', async () => {
178+
it('Browse Queues button navigates straight to the first queue', async () => {
179179
render(<DashboardPage />, { wrapper: createWrapper() });
180180
fireEvent.click(await screen.findByRole('button', { name: /browse queues/i }));
181-
expect(mockNavigate).toHaveBeenCalledWith('/messages?namespace=ns1');
181+
expect(mockNavigate).toHaveBeenCalledWith('/messages?namespace=ns1&queue=queue-1&queueType=active');
182182
});
183183

184184
it('View DLQ History button navigates to dlq-history page', async () => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ describe('DlqHistoryPage', () => {
106106
it('shows namespace name in subtitle when namespace resolved', () => {
107107
const Wrapper = createWrapper();
108108
render(<Wrapper><DlqHistoryPage /></Wrapper>);
109-
expect(screen.getByText(/My Namespace/)).toBeInTheDocument();
109+
// Name appears in the subtitle and again in the namespace widget strip
110+
expect(screen.getAllByText(/My Namespace/).length).toBeGreaterThanOrEqual(1);
110111
});
111112

112113
it('renders DlqHistoryTable', () => {
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { MemoryRouter } from 'react-router-dom';
4+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5+
import { MessagesOverviewPage } from '@/pages/MessagesOverviewPage';
6+
7+
vi.mock('@/hooks/useNamespaces', () => ({ useNamespaces: vi.fn() }));
8+
vi.mock('@/hooks/useQueues', () => ({ useQueues: vi.fn() }));
9+
vi.mock('@/hooks/useTopics', () => ({ useTopics: vi.fn() }));
10+
vi.mock('@/hooks/useSubscriptions', () => ({ useSubscriptions: vi.fn() }));
11+
12+
const mockNavigate = vi.fn();
13+
vi.mock('react-router-dom', async () => {
14+
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
15+
return { ...actual, useNavigate: () => mockNavigate };
16+
});
17+
18+
import { useNamespaces } from '@/hooks/useNamespaces';
19+
import { useQueues } from '@/hooks/useQueues';
20+
import { useTopics } from '@/hooks/useTopics';
21+
import { useSubscriptions } from '@/hooks/useSubscriptions';
22+
23+
const mockUseNamespaces = useNamespaces as ReturnType<typeof vi.fn>;
24+
const mockUseQueues = useQueues as ReturnType<typeof vi.fn>;
25+
const mockUseTopics = useTopics as ReturnType<typeof vi.fn>;
26+
const mockUseSubscriptions = useSubscriptions as ReturnType<typeof vi.fn>;
27+
28+
const azureNs = {
29+
id: 'ns-azure',
30+
name: 'sb-dev.servicebus.windows.net',
31+
displayName: 'Dev SB',
32+
isActive: true,
33+
environment: 'dev' as const,
34+
cloudProvider: 'azure' as const,
35+
hasListenPermission: true,
36+
hasSendPermission: true,
37+
hasManagePermission: true,
38+
createdAt: '2026-01-01T00:00:00Z',
39+
};
40+
41+
const awsNs = {
42+
...azureNs,
43+
id: 'ns-aws',
44+
name: 'sqs.ap-south-1.amazonaws.com',
45+
displayName: 'DevAWS',
46+
cloudProvider: 'aws' as const,
47+
};
48+
49+
const azureQueues = [
50+
{ name: 'orders', activeMessageCount: 4, deadLetterMessageCount: 1, scheduledMessageCount: 0, sizeInBytes: 0, status: 'Active' },
51+
];
52+
53+
const awsQueues = [
54+
{ name: 'sqs-orders', activeMessageCount: 2, deadLetterMessageCount: 3, scheduledMessageCount: 0, sizeInBytes: 0, status: 'Active', deadLetterTargetQueue: 'sqs-orders-dlq' },
55+
{ name: 'sqs-orders-dlq', activeMessageCount: 3, deadLetterMessageCount: 0, scheduledMessageCount: 0, sizeInBytes: 0, status: 'Active' },
56+
];
57+
58+
function renderPage(initialEntry = '/messages-overview') {
59+
const queryClient = new QueryClient({
60+
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
61+
});
62+
return render(
63+
<MemoryRouter initialEntries={[initialEntry]}>
64+
<QueryClientProvider client={queryClient}>
65+
<MessagesOverviewPage />
66+
</QueryClientProvider>
67+
</MemoryRouter>,
68+
);
69+
}
70+
71+
describe('MessagesOverviewPage', () => {
72+
beforeEach(() => {
73+
vi.clearAllMocks();
74+
mockUseNamespaces.mockReturnValue({ data: [azureNs, awsNs], isLoading: false });
75+
mockUseQueues.mockImplementation((id: string) => ({
76+
data: id === 'ns-aws' ? awsQueues : azureQueues,
77+
isLoading: false,
78+
isError: false,
79+
}));
80+
mockUseTopics.mockReturnValue({ data: [], isLoading: false });
81+
mockUseSubscriptions.mockReturnValue({ data: [], isLoading: false });
82+
});
83+
84+
it('renders a section per namespace across providers', () => {
85+
renderPage();
86+
expect(screen.getByText('Dev SB')).toBeInTheDocument();
87+
expect(screen.getByText('DevAWS')).toBeInTheDocument();
88+
expect(screen.getByText('Azure')).toBeInTheDocument();
89+
expect(screen.getByText('AWS')).toBeInTheDocument();
90+
});
91+
92+
it('hides AWS companion DLQ queues as standalone widgets', () => {
93+
renderPage();
94+
expect(screen.getByText('sqs-orders')).toBeInTheDocument();
95+
expect(screen.queryByText('sqs-orders-dlq')).not.toBeInTheDocument();
96+
});
97+
98+
it('navigates to the queue messages view on widget click (active tab)', () => {
99+
renderPage();
100+
fireEvent.click(screen.getByText('orders'));
101+
expect(mockNavigate).toHaveBeenCalledWith(
102+
'/messages?namespace=ns-azure&queue=orders&queueType=active',
103+
);
104+
});
105+
106+
it('navigates with deadletter queueType when the dead-letter tab is selected', () => {
107+
renderPage('/messages-overview?tab=deadletter');
108+
fireEvent.click(screen.getByText('sqs-orders'));
109+
expect(mockNavigate).toHaveBeenCalledWith(
110+
'/messages?namespace=ns-aws&queue=sqs-orders&queueType=deadletter',
111+
);
112+
});
113+
114+
it('shows the dead-letter header when tab=deadletter', () => {
115+
renderPage('/messages-overview?tab=deadletter');
116+
expect(screen.getByText('Dead-Letter Overview')).toBeInTheDocument();
117+
});
118+
119+
it('shows connect CTA when no namespaces exist', () => {
120+
mockUseNamespaces.mockReturnValue({ data: [], isLoading: false });
121+
renderPage();
122+
expect(screen.getByText('No namespaces connected')).toBeInTheDocument();
123+
});
124+
125+
it('sorts queues busiest-first and caps the grid height for many queues', () => {
126+
const manyQueues = Array.from({ length: 30 }, (_, i) => ({
127+
name: `q-${i}`,
128+
activeMessageCount: i,
129+
deadLetterMessageCount: 0,
130+
scheduledMessageCount: 0,
131+
sizeInBytes: 0,
132+
status: 'Active',
133+
}));
134+
mockUseNamespaces.mockReturnValue({ data: [azureNs], isLoading: false });
135+
mockUseQueues.mockReturnValue({ data: manyQueues, isLoading: false, isError: false });
136+
137+
const { container } = renderPage();
138+
139+
// Busiest queue (q-29) renders before the quietest (q-0)
140+
const labels = Array.from(container.querySelectorAll('section button span')).map(
141+
(el) => el.textContent,
142+
);
143+
expect(labels.indexOf('q-29')).toBeGreaterThan(-1);
144+
expect(labels.indexOf('q-29')).toBeLessThan(labels.indexOf('q-0'));
145+
// Grid scrolls inside a capped container instead of growing the page
146+
expect(container.querySelector('.max-h-56.overflow-y-auto')).toBeTruthy();
147+
expect(screen.getByText('Queues (30)')).toBeInTheDocument();
148+
});
149+
150+
it('search filters entities and hides namespaces without matches', () => {
151+
renderPage();
152+
fireEvent.change(screen.getByLabelText('Search entities'), { target: { value: 'sqs' } });
153+
expect(screen.getByText('sqs-orders')).toBeInTheDocument();
154+
// Azure namespace has no matching entities → its whole section disappears
155+
expect(screen.queryByText('Dev SB')).not.toBeInTheDocument();
156+
expect(screen.queryByText('orders')).not.toBeInTheDocument();
157+
});
158+
159+
it('sections collapse and expand from the header', () => {
160+
mockUseNamespaces.mockReturnValue({ data: [azureNs], isLoading: false });
161+
renderPage();
162+
expect(screen.getByText('orders')).toBeInTheDocument();
163+
fireEvent.click(screen.getByRole('button', { name: /Dev SB/ }));
164+
expect(screen.queryByText('orders')).not.toBeInTheDocument();
165+
fireEvent.click(screen.getByRole('button', { name: /Dev SB/ }));
166+
expect(screen.getByText('orders')).toBeInTheDocument();
167+
});
168+
});

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ describe('ScheduledMessagesPage', () => {
120120

121121
it('renders namespace selector with all namespaces', () => {
122122
renderPage();
123-
expect(screen.getByText('Prod Namespace')).toBeInTheDocument();
124-
expect(screen.getByText('Dev Namespace')).toBeInTheDocument();
123+
// Names appear in the dropdown and again in the provider widget strip
124+
expect(screen.getAllByText('Prod Namespace').length).toBeGreaterThanOrEqual(1);
125+
expect(screen.getAllByText('Dev Namespace').length).toBeGreaterThanOrEqual(1);
125126
});
126127

127128
it('shows placeholder when no namespace selected', () => {

apps/web/src/components/fab/MessageFAB.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ export function MessageFAB({
149149
'topic',
150150
subscriptionName
151151
);
152-
toast.success(`✅ Moved ${result.deadLetteredCount} messages to DLQ from ${topicName}/${subscriptionName}`);
152+
if (result.deadLetteredCount > 0) {
153+
toast.success(`✅ Moved ${result.deadLetteredCount} messages to DLQ from ${topicName}/${subscriptionName}`);
154+
}
153155
} else if (queueName) {
154156
// Dead-letter from queue
155157
result = await messagesApi.deadLetter(
@@ -160,20 +162,25 @@ export function MessageFAB({
160162
'Manually moved to DLQ for testing purposes via ServiceHub UI',
161163
'queue'
162164
);
163-
toast.success(`✅ Moved ${result.deadLetteredCount} messages to DLQ from ${queueName}`);
165+
if (result.deadLetteredCount > 0) {
166+
toast.success(`✅ Moved ${result.deadLetteredCount} messages to DLQ from ${queueName}`);
167+
}
164168
}
165169

166-
if (result && result.deadLetteredCount > 0) {
167-
// Refresh to show updated counts immediately with refetch
170+
if (result) {
171+
// Refresh even when nothing was moved: a 0-count usually means the queue
172+
// is empty (e.g. an external consumer drained it), so the currently
173+
// displayed message list is a stale snapshot that must be reconciled.
168174
await Promise.all([
169175
queryClient.invalidateQueries({ queryKey: ['messages'], refetchType: 'active' }),
170176
queryClient.invalidateQueries({ queryKey: ['queues', namespaceId], refetchType: 'active' }),
171177
queryClient.invalidateQueries({ queryKey: ['topics', namespaceId], refetchType: 'active' }),
172178
queryClient.invalidateQueries({ queryKey: ['subscriptions', namespaceId], refetchType: 'active' }),
173179
]);
174-
} else if (result && result.deadLetteredCount === 0) {
180+
}
181+
if (result && result.deadLetteredCount === 0) {
175182
toast(
176-
'No active messages were available to dead-letter. If you just sent messages, an external consumer may be draining this queue.',
183+
'No active messages were available to dead-letter — the queue is empty right now. The list has been refreshed; if you just sent messages, an external consumer may be draining this queue.',
177184
{ icon: 'ℹ️', duration: 6000 }
178185
);
179186
}

apps/web/src/components/layout/MainLayout.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,18 @@ export function MainLayout() {
9393
useEffect(() => {
9494
setThemeProvider(currentNamespace?.cloudProvider);
9595
}, [currentNamespace?.cloudProvider]);
96+
// Quick Access pages keep the neutral light-blue chrome; the provider tint
97+
// (AWS orange / GCP green) applies only once the user moves into a cloud's
98+
// message space. The sticky provider signal still updates in the background
99+
// so the tint is correct the moment a messages view opens.
100+
const inProviderSpace = window.location.pathname.endsWith('/messages');
101+
const appliedTheme = inProviderSpace ? themeProvider : 'azure';
96102
useEffect(() => {
97-
document.documentElement.dataset.provider = themeProvider;
103+
document.documentElement.dataset.provider = appliedTheme;
98104
return () => {
99105
delete document.documentElement.dataset.provider;
100106
};
101-
}, [themeProvider]);
107+
}, [appliedTheme]);
102108
// FAB: only in DEV environment with Manage (write) permission — never in UAT/Prod or read-only connections
103109
const canUseFab = currentNamespace?.environment === 'dev' && currentNamespace?.hasManagePermission === true;
104110

apps/web/src/components/layout/Sidebar.tsx

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
FlaskConical,
2020
Route,
2121
} from 'lucide-react';
22-
import toast from 'react-hot-toast';
2322
import { useState } from 'react';
2423
import { useNamespaces } from '@/hooks/useNamespaces';
2524
import { useQueues } from '@/hooks/useQueues';
@@ -409,9 +408,6 @@ export function Sidebar() {
409408
// Get active namespace for Quick Access
410409
const activeNamespace = namespaces?.find(ns => ns.isActive);
411410

412-
// Fetch queues and topics for Quick Access buttons
413-
const { data: queues } = useQueues(activeNamespace?.id || '');
414-
const { data: topics } = useTopics(activeNamespace?.id || '');
415411

416412
// In demo mode, use mock stats; otherwise use real API stats
417413
const demoStats = isDemoMode && cloudProvider ? getMockStats(cloudProvider) : null;
@@ -533,65 +529,23 @@ export function Sidebar() {
533529
)}
534530
</NavLink>
535531

532+
{/* Multi-cloud overviews: every namespace (Azure, AWS, GCP) with its
533+
entities as clickable widgets — no provider is favoured. */}
536534
<button
537-
onClick={() => {
538-
const activeNamespace = namespaces?.find(ns => ns.isActive);
539-
if (!activeNamespace) {
540-
toast.error('No active namespace selected');
541-
return;
542-
}
543-
544-
// Navigate to first queue if available
545-
const firstQueue = queues?.[0];
546-
if (firstQueue) {
547-
navigate(`${navPrefix}/messages?namespace=${activeNamespace.id}&queue=${firstQueue.name}&queueType=active`);
548-
return;
549-
}
550-
551-
// If no queues, check for topics (user will need to select subscription)
552-
const firstTopic = topics?.[0];
553-
if (firstTopic) {
554-
toast('Select a subscription from the topic to view messages', { icon: 'ℹ️' });
555-
return;
556-
}
557-
558-
toast('No queues or topics available', { icon: 'ℹ️' });
559-
}}
535+
onClick={() => navigate(`${navPrefix}/messages-overview?tab=active`)}
560536
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all bg-white hover:bg-sky-50 text-gray-700 hover:text-sky-700 border border-gray-200 hover:border-sky-300 shadow-sm"
561537
>
562538
<Database className="w-4 h-4 text-sky-500" />
563539
<span className="flex-1 text-left">Active Messages</span>
564-
<span className="text-xs text-sky-600 font-medium">View All</span>
540+
<span className="text-xs text-sky-600 font-medium">All Clouds</span>
565541
</button>
566542
<button
567-
onClick={() => {
568-
const activeNamespace = namespaces?.find(ns => ns.isActive);
569-
if (!activeNamespace) {
570-
toast.error('No active namespace selected');
571-
return;
572-
}
573-
574-
// Navigate to first queue's DLQ if available
575-
const firstQueue = queues?.[0];
576-
if (firstQueue) {
577-
navigate(`${navPrefix}/messages?namespace=${activeNamespace.id}&queue=${firstQueue.name}&queueType=deadletter`);
578-
return;
579-
}
580-
581-
// If no queues, check for topics
582-
const firstTopic = topics?.[0];
583-
if (firstTopic) {
584-
toast('Select a subscription from the topic to view DLQ', { icon: '⚠️' });
585-
return;
586-
}
587-
588-
toast('No queues or topics available for DLQ view', { icon: '⚠️' });
589-
}}
543+
onClick={() => navigate(`${navPrefix}/messages-overview?tab=deadletter`)}
590544
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all bg-white hover:bg-red-50 text-gray-700 hover:text-red-700 border border-gray-200 hover:border-red-300 shadow-sm"
591545
>
592546
<AlertCircle className="w-4 h-4 text-red-500" />
593547
<span className="flex-1 text-left">Dead-Letter</span>
594-
<span className="text-xs text-red-600 font-medium">DLQ</span>
548+
<span className="text-xs text-red-600 font-medium">All Clouds</span>
595549
</button>
596550
<NavLink
597551
to={activeNamespace ? `${navPrefix}/dlq-history?namespace=${activeNamespace.id}` : `${navPrefix}/dlq-history`}

0 commit comments

Comments
 (0)