Skip to content

Commit dd6ed6e

Browse files
ENG-39 Create new job on trigger for a full policy regen (#1701)
* feat(app): add option to regenerate all policies * fix(app): rename 'Regenerate full policies' to 'Regenerate all policies' * fix(app): rename description on policy regen dialog by removing 'mark it for review' --------- Co-authored-by: chasprowebdev <chasgarciaprowebdev@gmail.com>
1 parent 73a0680 commit dd6ed6e

5 files changed

Lines changed: 170 additions & 2 deletions

File tree

‎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)