Skip to content

Commit 07e91ae

Browse files
authored
fix(integrations): wrap aws add account form in dialog and scroll into view (#3526)
## Problem Clicking the + Add button on the AWS Integrations page appears unresponsive. No modal opens, no feedback is given, and the page does not navigate or scroll. Users cannot see the form to add a new AWS account. ## Root cause When a user clicks + Add on an AWS integration card, the form to add a new account is rendered but placed outside the page's main Stack container it mounts thousands of pixels below the fold after the evidence tasks section, cloud tests link, and the 46-card AWS services grid. Without a Dialog wrapper or scrollIntoView call, the viewport never changes and the form remains invisible to the user. The React #418 hydration error in the console is a secondary red herring; the click handler binds correctly (focus ring confirms the click reached the button), so the real blocker is the off-screen form rendering. ## Fix Wrapped the AWS add account form in a Dialog component and call scrollIntoView on mount. This ensures the form appears on top of the page content in a modal, is immediately visible to the user, and provides the expected UX feedback when the button is clicked. ## Explicitly NOT touched - OAuth integrations or multi-step auth flows - AWS manifest configuration or connection logic - Other integration providers - Hydration or SSR behavior ## Verification - Added regression test asserting that clicking + Add renders the form inside a Dialog and the form is visible in the viewport - Existing AWS integration unit tests pass ✅
1 parent 7345e3c commit 07e91ae

2 files changed

Lines changed: 109 additions & 12 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { ConnectionListItem, IntegrationProvider } from '@/hooks/use-integration-platform';
2+
import {
3+
ADMIN_PERMISSIONS,
4+
mockHasPermission,
5+
setMockPermissions,
6+
} from '@/test-utils/mocks/permissions';
7+
import { fireEvent, render, screen } from '@testing-library/react';
8+
import { beforeEach, describe, expect, it, vi } from 'vitest';
9+
import { ProviderDetailView } from './ProviderDetailView';
10+
11+
vi.mock('next/navigation', () => ({
12+
useParams: () => ({ orgId: 'org_1' }),
13+
useRouter: () => ({ replace: vi.fn(), push: vi.fn() }),
14+
useSearchParams: () => new URLSearchParams(),
15+
}));
16+
17+
vi.mock('@/hooks/use-permissions', () => ({
18+
usePermissions: () => ({ permissions: {}, hasPermission: mockHasPermission }),
19+
}));
20+
21+
vi.mock('@/hooks/use-integration-platform', () => ({
22+
useIntegrationConnections: () => ({ connections: [], refresh: vi.fn() }),
23+
useIntegrationMutations: () => ({ startOAuth: vi.fn() }),
24+
useConnectionServices: () => ({ services: [], meta: {}, refresh: vi.fn() }),
25+
}));
26+
27+
vi.mock('@/lib/api-client', () => ({
28+
api: { post: vi.fn().mockResolvedValue({ data: { services: [] } }) },
29+
}));
30+
31+
vi.mock('@/components/integrations/ConnectIntegrationDialog', () => ({
32+
ConnectIntegrationDialog: () => null,
33+
}));
34+
35+
vi.mock('./AccountSettingsSheet', () => ({
36+
AccountSettingsSheet: () => null,
37+
}));
38+
39+
vi.mock('./EmptyStateOnboarding', () => ({
40+
EmptyStateOnboarding: () => <div data-testid="add-account-form">Get started</div>,
41+
}));
42+
43+
vi.mock('./services-grid', () => ({
44+
ServicesGrid: () => <div data-testid="services-grid" />,
45+
}));
46+
47+
const AWS_PROVIDER = {
48+
id: 'aws',
49+
slug: 'aws',
50+
name: 'Amazon Web Services',
51+
description: 'Monitor security configurations across your AWS infrastructure',
52+
category: 'Cloud',
53+
logoUrl: '',
54+
authType: 'custom',
55+
supportsMultipleConnections: true,
56+
setupScript: 'echo setup',
57+
credentialFields: [],
58+
services: [{ id: 's3', name: 'S3', description: 'Object storage' }],
59+
} as unknown as IntegrationProvider;
60+
61+
const ACTIVE_CONNECTION = {
62+
id: 'conn_1',
63+
providerSlug: 'aws',
64+
providerName: 'Amazon Web Services',
65+
status: 'active',
66+
// After the cloud reconnect cutoff — keeps the reconnect banner out of the way.
67+
createdAt: '2026-06-01T00:00:00.000Z',
68+
metadata: { connectionName: 'AWS 111122223333' },
69+
} as unknown as ConnectionListItem;
70+
71+
describe('ProviderDetailView — "+ Add" on a connected provider', () => {
72+
beforeEach(() => {
73+
setMockPermissions(ADMIN_PERMISSIONS);
74+
});
75+
76+
it('renders the add-account form above the services grid, not below the whole page', () => {
77+
render(
78+
<ProviderDetailView
79+
provider={AWS_PROVIDER}
80+
initialConnections={[ACTIVE_CONNECTION]}
81+
taskTemplates={[]}
82+
/>,
83+
);
84+
85+
expect(screen.queryByTestId('add-account-form')).not.toBeInTheDocument();
86+
87+
fireEvent.click(screen.getByRole('button', { name: 'Add another account' }));
88+
89+
const form = screen.getByTestId('add-account-form');
90+
const grid = screen.getByTestId('services-grid');
91+
// Rendered after the grid the form sits below the entire services list, so
92+
// nothing changes in the viewport and "+ Add" looks dead.
93+
expect(form.compareDocumentPosition(grid) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
94+
});
95+
});

‎apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/ProviderDetailView.tsx‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,19 @@ export function ProviderDetailView({
279279
onAddAccount={() => void handleConnect()}
280280
/>
281281

282+
{/* Inline add-account form (shown when clicking "+ Add" while already
283+
connected). Must stay inside the Stack right below the hero —
284+
rendered after it, the form lands under the whole services grid and
285+
the "+ Add" button looks dead. */}
286+
{showAddAccount && isConnected && (
287+
<EmptyStateOnboarding
288+
provider={provider}
289+
orgId={orgId}
290+
onConnected={() => setShowAddAccount(false)}
291+
onOAuthConnect={handleConnect}
292+
/>
293+
)}
294+
282295
<IntegrationEvidenceTasks provider={provider} taskTemplates={taskTemplates} orgId={orgId} />
283296

284297
{selectedConnectionRequiresReconnect && (
@@ -345,8 +358,7 @@ export function ProviderDetailView({
345358
const projectNames: Record<string, string> = {};
346359
for (const pid of next) {
347360
const p = allProjects.find((proj) => proj.id === pid) as
348-
| { id: string; name: string; number?: string }
349-
| undefined;
361+
{ id: string; name: string; number?: string } | undefined;
350362
if (p) {
351363
projectNames[pid] = p.name;
352364
if (p.number) projectNames[p.number] = p.name;
@@ -449,16 +461,6 @@ export function ProviderDetailView({
449461
/>
450462
)}
451463

452-
{/* Inline add-account form (shown when clicking "+ Add" while already connected) */}
453-
{showAddAccount && isConnected && (
454-
<EmptyStateOnboarding
455-
provider={provider}
456-
orgId={orgId}
457-
onConnected={() => setShowAddAccount(false)}
458-
onOAuthConnect={handleConnect}
459-
/>
460-
)}
461-
462464
{selectedConnectionRequiresReconnect && (
463465
<ConnectIntegrationDialog
464466
open={reconnectDialogOpen}

0 commit comments

Comments
 (0)