Skip to content

Commit 27a006f

Browse files
authored
Merge pull request #2527 from trycompai/fix/integrations-setup-cta
feat(app): flag pre-cutoff cloud connections for reconnect
2 parents 8785c18 + a5f59a7 commit 27a006f

File tree

5 files changed

+171
-0
lines changed

5 files changed

+171
-0
lines changed

apps/app/src/app/(app)/[orgId]/cloud-tests/components/ProviderTabs.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useApi } from '@/hooks/use-api';
22
import { useConnectionServices } from '@/hooks/use-integration-platform';
3+
import { CLOUD_RECONNECT_CUTOFF_LABEL } from '@/lib/cloud-reconnect-policy';
34
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsContent, TabsList, TabsTrigger } from '@trycompai/design-system';
45
import { Add } from '@trycompai/design-system/icons';
56
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -24,6 +25,7 @@ interface ProviderTabsProps {
2425
onAddConnection: (providerType: string) => void;
2526
onConfigure: (provider: Provider) => void;
2627
needsConfiguration: (provider: Provider) => boolean;
28+
requiresReconnect: (provider: Provider) => boolean;
2729
canRunScan?: boolean;
2830
canAddConnection?: boolean;
2931
orgId: string;
@@ -218,6 +220,7 @@ export function ProviderTabs({
218220
onAddConnection,
219221
onConfigure,
220222
needsConfiguration,
223+
requiresReconnect,
221224
canRunScan,
222225
canAddConnection,
223226
orgId,
@@ -293,9 +296,29 @@ export function ProviderTabs({
293296
</div>
294297

295298
{connections.map((connection) => {
299+
const reconnectRequired = requiresReconnect(connection);
300+
296301
return (
297302
<TabsContent key={connection.id} value={connection.id}>
298303
<div className="mt-4">
304+
{reconnectRequired && (
305+
<div className="mb-4 rounded-lg border border-warning/30 bg-warning/10 px-4 py-3">
306+
<div className="flex items-center justify-between gap-3">
307+
<div>
308+
<p className="text-sm font-medium">Reconnect this account</p>
309+
<p className="text-xs text-muted-foreground mt-0.5">
310+
This connection was created before {CLOUD_RECONNECT_CUTOFF_LABEL}. Reconnect it to keep scans and remediation fully reliable.
311+
</p>
312+
</div>
313+
{canAddConnection !== false && (
314+
<Button size="sm" onClick={() => onAddConnection(providerType)}>
315+
Reconnect
316+
</Button>
317+
)}
318+
</div>
319+
</div>
320+
)}
321+
299322
<ConnectionDetails connection={connection} />
300323

301324
{/* New platform connections get full tabbed UI */}

apps/app/src/app/(app)/[orgId]/cloud-tests/components/TestsLayout.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ConnectIntegrationDialog } from '@/components/integrations/ConnectInteg
44
import { useApi } from '@/hooks/use-api';
55
import { usePermissions } from '@/hooks/use-permissions';
66
import { ManageIntegrationDialog } from '@/components/integrations/ManageIntegrationDialog';
7+
import { CLOUD_RECONNECT_CUTOFF_LABEL, requiresCloudReconnect } from '@/lib/cloud-reconnect-policy';
78
import { Button, PageHeader, PageHeaderDescription, PageLayout } from '@trycompai/design-system';
89
import { Add, Settings } from '@trycompai/design-system/icons';
910
import { useSearchParams, useRouter } from 'next/navigation';
@@ -101,6 +102,18 @@ export function TestsLayout({ initialFindings, initialProviders, orgId }: TestsL
101102
const isProvidersValidating = providersResponse.isValidating;
102103

103104
const connectedProviders = providers;
105+
const reconnectRequiredCount = useMemo(
106+
() =>
107+
connectedProviders.filter((provider) =>
108+
requiresCloudReconnect({
109+
providerId: provider.integrationId,
110+
createdAt: provider.createdAt,
111+
isLegacy: provider.isLegacy,
112+
status: provider.status,
113+
}),
114+
).length,
115+
[connectedProviders],
116+
);
104117

105118
// Group connections by provider type (aws, gcp, azure)
106119
const providerGroups = useMemo(() => {
@@ -269,6 +282,17 @@ export function TestsLayout({ initialFindings, initialProviders, orgId }: TestsL
269282
<PageHeaderDescription>{multiProviderDescription}</PageHeaderDescription>
270283
</PageHeader>
271284

285+
{reconnectRequiredCount > 0 && (
286+
<div className="mb-4 rounded-lg border border-warning/30 bg-warning/10 px-4 py-3">
287+
<p className="text-sm font-medium text-foreground">
288+
Reconnect required for {reconnectRequiredCount} cloud connection{reconnectRequiredCount === 1 ? '' : 's'}
289+
</p>
290+
<p className="text-xs text-muted-foreground mt-0.5">
291+
Connections created before {CLOUD_RECONNECT_CUTOFF_LABEL} should be re-added to keep scans and remediation fully reliable.
292+
</p>
293+
</div>
294+
)}
295+
272296
<ProviderTabs
273297
providerGroups={providerGroups}
274298
providerTypes={activeProviderTypes}
@@ -303,6 +327,14 @@ export function TestsLayout({ initialFindings, initialProviders, orgId }: TestsL
303327
setConfigureDialogOpen(true);
304328
}}
305329
needsConfiguration={needsVariableConfiguration}
330+
requiresReconnect={(provider) =>
331+
requiresCloudReconnect({
332+
providerId: provider.integrationId,
333+
createdAt: provider.createdAt,
334+
isLegacy: provider.isLegacy,
335+
status: provider.status,
336+
})
337+
}
306338
canRunScan={canRunScan}
307339
canAddConnection={canCreateIntegration}
308340
orgId={orgId}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
type ConnectionListItem,
88
type IntegrationProvider,
99
} from '@/hooks/use-integration-platform';
10+
import {
11+
CLOUD_RECONNECT_CUTOFF_LABEL,
12+
requiresCloudReconnect,
13+
} from '@/lib/cloud-reconnect-policy';
1014
import { api } from '@/lib/api-client';
1115
import {
1216
Breadcrumb,
@@ -63,6 +67,14 @@ export function ProviderDetailView({ provider, initialConnections }: ProviderDet
6367
}
6468
).services ?? [];
6569
const isCloudProvider = provider.category === 'Cloud';
70+
const selectedConnectionRequiresReconnect = useMemo(() => {
71+
if (!isCloudProvider || !selectedConnection) return false;
72+
return requiresCloudReconnect({
73+
providerId: provider.id,
74+
createdAt: selectedConnection.createdAt,
75+
status: selectedConnection.status,
76+
});
77+
}, [isCloudProvider, provider.id, selectedConnection]);
6678

6779
// Services hook for the selected connection
6880
const {
@@ -182,6 +194,20 @@ export function ProviderDetailView({ provider, initialConnections }: ProviderDet
182194
onAddAccount={() => void handleConnect()}
183195
/>
184196

197+
{selectedConnectionRequiresReconnect && (
198+
<div className="rounded-lg border border-warning/30 bg-warning/10 px-4 py-3 flex items-center justify-between gap-3">
199+
<div>
200+
<p className="text-sm font-medium">Reconnect this account</p>
201+
<p className="text-xs text-muted-foreground mt-0.5">
202+
This connection was created before {CLOUD_RECONNECT_CUTOFF_LABEL}. Reconnect it to keep scans and remediation fully reliable.
203+
</p>
204+
</div>
205+
<Button size="sm" variant="outline" onClick={() => void handleConnect()}>
206+
Reconnect
207+
</Button>
208+
</div>
209+
)}
210+
185211
{/* Content: zero state OR findings */}
186212
{!isConnected && (
187213
<EmptyStateOnboarding
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
CLOUD_RECONNECT_CUTOFF_ISO_UTC,
4+
requiresCloudReconnect,
5+
} from './cloud-reconnect-policy';
6+
7+
describe('requiresCloudReconnect', () => {
8+
it('returns true for cloud connections created before the cutoff date', () => {
9+
expect(
10+
requiresCloudReconnect({
11+
providerId: 'aws',
12+
createdAt: '2026-04-12T23:59:59.999Z',
13+
status: 'active',
14+
}),
15+
).toBe(true);
16+
});
17+
18+
it('returns false for cloud connections created exactly at the cutoff timestamp', () => {
19+
expect(
20+
requiresCloudReconnect({
21+
providerId: 'aws',
22+
createdAt: CLOUD_RECONNECT_CUTOFF_ISO_UTC,
23+
status: 'active',
24+
}),
25+
).toBe(false);
26+
});
27+
28+
it('returns false for cloud connections created after the cutoff date', () => {
29+
expect(
30+
requiresCloudReconnect({
31+
providerId: 'gcp',
32+
createdAt: '2026-04-14T00:00:00.000Z',
33+
status: 'active',
34+
}),
35+
).toBe(false);
36+
});
37+
38+
it('returns false for non-cloud providers', () => {
39+
expect(
40+
requiresCloudReconnect({
41+
providerId: 'github',
42+
createdAt: CLOUD_RECONNECT_CUTOFF_ISO_UTC,
43+
status: 'active',
44+
}),
45+
).toBe(false);
46+
});
47+
48+
it('returns false for invalid dates', () => {
49+
expect(
50+
requiresCloudReconnect({
51+
providerId: 'azure',
52+
createdAt: 'not-a-date',
53+
status: 'active',
54+
}),
55+
).toBe(false);
56+
});
57+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const CLOUD_RECONNECT_PROVIDER_IDS = new Set(['aws', 'gcp', 'azure']);
2+
3+
/**
4+
* Connections created before this UTC timestamp require re-connection.
5+
* This rollout date is fixed intentionally so the behavior is stable over time.
6+
*/
7+
export const CLOUD_RECONNECT_CUTOFF_ISO_UTC = '2026-04-13T00:00:00.000Z';
8+
export const CLOUD_RECONNECT_CUTOFF_LABEL = 'April 13, 2026';
9+
10+
const CLOUD_RECONNECT_CUTOFF_MS = new Date(CLOUD_RECONNECT_CUTOFF_ISO_UTC).getTime();
11+
12+
type ReconnectCandidate = {
13+
providerId: string;
14+
createdAt?: Date | string | null;
15+
isLegacy?: boolean;
16+
status?: string | null;
17+
};
18+
19+
export function requiresCloudReconnect(candidate: ReconnectCandidate): boolean {
20+
if (!CLOUD_RECONNECT_PROVIDER_IDS.has(candidate.providerId)) return false;
21+
if (candidate.isLegacy) return false;
22+
23+
if (candidate.status && candidate.status !== 'active' && candidate.status !== 'pending') {
24+
return false;
25+
}
26+
27+
if (!candidate.createdAt) return false;
28+
29+
const createdAt = new Date(candidate.createdAt);
30+
if (Number.isNaN(createdAt.getTime())) return false;
31+
32+
return createdAt.getTime() < CLOUD_RECONNECT_CUTOFF_MS;
33+
}

0 commit comments

Comments
 (0)