Skip to content

Commit 646fdb3

Browse files
committed
chore: merge main into release for new releases
2 parents 7c3748b + 9abfc4a commit 646fdb3

File tree

6 files changed

+223
-5
lines changed

6 files changed

+223
-5
lines changed

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Button } from '@comp/ui/button';
44
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select';
55
import { useRealtimeRun } from '@trigger.dev/react-hooks';
6-
import { CheckCircle2, Loader2, RefreshCw, X } from 'lucide-react';
6+
import { CheckCircle2, Info, Loader2, RefreshCw, X } from 'lucide-react';
77
import { useEffect, useState } from 'react';
88
import { FindingsTable } from './FindingsTable';
99

@@ -27,6 +27,23 @@ interface ResultsViewProps {
2727

2828
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
2929

30+
// Helper function to extract clean error messages from cloud provider errors
31+
function extractCleanErrorMessage(errorMessage: string): string {
32+
try {
33+
// Try to parse as JSON (GCP returns JSON blob)
34+
const parsed = JSON.parse(errorMessage);
35+
36+
// GCP error structure: { error: { message: "actual message" } }
37+
if (parsed.error?.message) {
38+
return parsed.error.message;
39+
}
40+
} catch {
41+
// Not JSON, return original
42+
}
43+
44+
return errorMessage;
45+
}
46+
3047
export function ResultsView({
3148
findings,
3249
scanTaskId,
@@ -46,6 +63,7 @@ export function ResultsView({
4663
const [selectedStatus, setSelectedStatus] = useState<string>('all');
4764
const [selectedSeverity, setSelectedSeverity] = useState<string>('all');
4865
const [showSuccessBanner, setShowSuccessBanner] = useState(false);
66+
const [showErrorBanner, setShowErrorBanner] = useState(false);
4967

5068
// Show success banner when scan completes, auto-hide after 5 seconds
5169
useEffect(() => {
@@ -58,6 +76,17 @@ export function ResultsView({
5876
}
5977
}, [scanCompleted]);
6078

79+
// Auto-dismiss error banner after 30 seconds
80+
useEffect(() => {
81+
if (scanFailed) {
82+
setShowErrorBanner(true);
83+
const timer = setTimeout(() => {
84+
setShowErrorBanner(false);
85+
}, 30000);
86+
return () => clearTimeout(timer);
87+
}
88+
}, [scanFailed]);
89+
6190
// Get unique statuses and severities
6291
const uniqueStatuses = Array.from(
6392
new Set(findings.map((f) => f.status).filter(Boolean) as string[]),
@@ -117,15 +146,36 @@ export function ResultsView({
117146
</div>
118147
)}
119148

120-
{scanFailed && !isScanning && (
149+
{/* Propagation delay info banner - only when scan succeeds but returns empty output */}
150+
{scanCompleted && findings.length === 0 && !isScanning && !scanFailed && (
151+
<div className="bg-blue-50 dark:bg-blue-950/20 flex items-center gap-3 rounded-lg border border-blue-200 dark:border-blue-900 p-4">
152+
<Info className="text-blue-600 dark:text-blue-400 h-5 w-5 flex-shrink-0" />
153+
<div className="flex-1">
154+
<p className="text-blue-900 dark:text-blue-100 text-sm font-medium">Initial scan complete</p>
155+
<p className="text-muted-foreground text-xs">
156+
Security findings may take 24-48 hours to appear after enabling cloud security services. Check back later.
157+
</p>
158+
</div>
159+
</div>
160+
)}
161+
162+
{showErrorBanner && scanFailed && !isScanning && (
121163
<div className="bg-destructive/10 flex items-center gap-3 rounded-lg border border-destructive/20 p-4">
122164
<X className="text-destructive h-5 w-5 flex-shrink-0" />
123165
<div className="flex-1">
124166
<p className="text-destructive text-sm font-medium">Scan failed</p>
125167
<p className="text-muted-foreground text-xs">
126-
{run?.error?.message || 'An error occurred during the scan. Please try again.'}
168+
{extractCleanErrorMessage(run?.error?.message || 'An error occurred during the scan. Please try again.')}
127169
</p>
128170
</div>
171+
<Button
172+
variant="ghost"
173+
size="sm"
174+
onClick={() => setShowErrorBanner(false)}
175+
className="text-muted-foreground hover:text-foreground h-auto p-1"
176+
>
177+
<X className="h-4 w-4" />
178+
</Button>
129179
</div>
130180
)}
131181

apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export function PolicyOverview({
231231
toast.info('Regeneration started');
232232
}}
233233
title="Regenerate Policy"
234-
description="This will regenerate the policy content and mark it for review. Continue?"
234+
description="This will regenerate the policy content. Continue?"
235235
confirmText="Regenerate"
236236
confirmIcon={<Icons.AI className="h-4 w-4" />}
237237
/>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use server';
2+
3+
import { authActionClient } from '@/actions/safe-action';
4+
import { generateFullPolicies } from '@/jobs/tasks/onboarding/generate-full-policies';
5+
import { tasks } from '@trigger.dev/sdk';
6+
import { z } from 'zod';
7+
8+
export const regenerateFullPoliciesAction = authActionClient
9+
.inputSchema(z.object({}))
10+
.metadata({
11+
name: 'regenerate-full-policies',
12+
track: {
13+
event: 'regenerate-full-policies',
14+
channel: 'server',
15+
},
16+
})
17+
.action(async ({ ctx }) => {
18+
const { session } = ctx;
19+
20+
if (!session?.activeOrganizationId) {
21+
throw new Error('No active organization');
22+
}
23+
24+
await tasks.trigger<typeof generateFullPolicies>('generate-full-policies', {
25+
organizationId: session.activeOrganizationId,
26+
});
27+
28+
// Revalidation handled by safe-action middleware using x-pathname header
29+
return { success: true };
30+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use client';
2+
3+
import { Button } from '@comp/ui/button';
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@comp/ui/dialog';
12+
import {
13+
DropdownMenu,
14+
DropdownMenuContent,
15+
DropdownMenuItem,
16+
DropdownMenuTrigger,
17+
} from '@comp/ui/dropdown-menu';
18+
import { Icons } from '@comp/ui/icons';
19+
import { useAction } from 'next-safe-action/hooks';
20+
import { useState } from 'react';
21+
import { toast } from 'sonner';
22+
import { regenerateFullPoliciesAction } from '../actions/regenerate-full-policies';
23+
24+
export function FullPolicyHeaderActions() {
25+
const [isRegenerateConfirmOpen, setRegenerateConfirmOpen] = useState(false);
26+
27+
const regenerate = useAction(regenerateFullPoliciesAction, {
28+
onSuccess: () => {
29+
toast.success('Policy regeneration started. This may take a few minutes.');
30+
setRegenerateConfirmOpen(false);
31+
},
32+
onError: (error) => {
33+
toast.error(error.error.serverError || 'Failed to regenerate policies');
34+
},
35+
});
36+
37+
const handleRegenerate = async () => {
38+
await regenerate.execute({});
39+
};
40+
41+
return (
42+
<>
43+
<DropdownMenu>
44+
<DropdownMenuTrigger asChild>
45+
<Button size="icon" variant="ghost" className="m-0 size-auto p-2">
46+
<Icons.Settings className="h-4 w-4" />
47+
</Button>
48+
</DropdownMenuTrigger>
49+
<DropdownMenuContent align="end">
50+
<DropdownMenuItem onClick={() => setRegenerateConfirmOpen(true)}>
51+
<Icons.AI className="mr-2 h-4 w-4" /> Regenerate all policies
52+
</DropdownMenuItem>
53+
</DropdownMenuContent>
54+
</DropdownMenu>
55+
56+
{/* Regenerate Confirmation Dialog */}
57+
<Dialog open={isRegenerateConfirmOpen} onOpenChange={setRegenerateConfirmOpen}>
58+
<DialogContent>
59+
<DialogHeader>
60+
<DialogTitle>Regenerate All Policies</DialogTitle>
61+
<DialogDescription>
62+
This will generate new policy content for all policies using your org context and
63+
frameworks. Continue?
64+
</DialogDescription>
65+
</DialogHeader>
66+
<DialogFooter>
67+
<Button
68+
variant="outline"
69+
onClick={() => setRegenerateConfirmOpen(false)}
70+
disabled={regenerate.status === 'executing'}
71+
>
72+
Cancel
73+
</Button>
74+
<Button onClick={handleRegenerate} disabled={regenerate.status === 'executing'}>
75+
{regenerate.status === 'executing' ? 'Working…' : 'Confirm'}
76+
</Button>
77+
</DialogFooter>
78+
</DialogContent>
79+
</Dialog>
80+
</>
81+
);
82+
}

apps/app/src/app/(app)/[orgId]/policies/all/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb';
22
import { getValidFilters } from '@/lib/data-table';
33
import type { SearchParams } from '@/types';
44
import type { Metadata } from 'next';
5+
import { FullPolicyHeaderActions } from './components/FullPolicyHeaderActions';
56
import { PoliciesTable } from './components/policies-table';
67
import { getPolicies } from './data/queries';
78
import { searchParamsCache } from './data/validations';
@@ -23,7 +24,10 @@ export default async function PoliciesPage({ ...props }: PolicyTableProps) {
2324
]);
2425

2526
return (
26-
<PageWithBreadcrumb breadcrumbs={[{ label: 'Policies', current: true }]}>
27+
<PageWithBreadcrumb
28+
breadcrumbs={[{ label: 'Policies', current: true }]}
29+
headerRight={<FullPolicyHeaderActions />}
30+
>
2731
<PoliciesTable promises={promises} />
2832
</PageWithBreadcrumb>
2933
);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { db } from '@db';
2+
import { logger, queue, task } from '@trigger.dev/sdk';
3+
import { getOrganizationContext, triggerPolicyUpdates } from './onboard-organization-helpers';
4+
5+
// v4 queues must be declared in advance
6+
const generateFullPoliciesQueue = queue({
7+
name: 'generate-full-policies',
8+
concurrencyLimit: 100,
9+
});
10+
11+
export const generateFullPolicies = task({
12+
id: 'generate-full-policies',
13+
queue: generateFullPoliciesQueue,
14+
retry: {
15+
maxAttempts: 3,
16+
},
17+
run: async (payload: { organizationId: string }) => {
18+
logger.info(`Starting full policy generation for organization ${payload.organizationId}`);
19+
20+
try {
21+
// Get organization context
22+
const { questionsAndAnswers } = await getOrganizationContext(payload.organizationId);
23+
24+
// Get frameworks
25+
const frameworkInstances = await db.frameworkInstance.findMany({
26+
where: {
27+
organizationId: payload.organizationId,
28+
},
29+
});
30+
31+
const frameworks = await db.frameworkEditorFramework.findMany({
32+
where: {
33+
id: {
34+
in: frameworkInstances.map((instance) => instance.frameworkId),
35+
},
36+
},
37+
});
38+
39+
// Trigger policy updates for all policies
40+
await triggerPolicyUpdates(payload.organizationId, questionsAndAnswers, frameworks);
41+
42+
logger.info(
43+
`Successfully triggered policy updates for organization ${payload.organizationId}`,
44+
);
45+
} catch (error) {
46+
logger.error(`Error during policy generation for organization ${payload.organizationId}:`, {
47+
error: error instanceof Error ? error.message : String(error),
48+
});
49+
throw error;
50+
}
51+
},
52+
});

0 commit comments

Comments
 (0)