Skip to content

Commit 002bb3e

Browse files
feat(risk, vendor): enhance real-time updates with SWR polling intervals (#1979)
Co-authored-by: Tofik Hasanov <annexcies@gmail.com>
1 parent d3f424e commit 002bb3e

File tree

8 files changed

+67
-29
lines changed

8 files changed

+67
-29
lines changed

apps/app/src/app/(app)/[orgId]/risk/(overview)/RisksTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,11 @@ export const RisksTable = ({
102102
return await getRisksAction({ orgId, searchParams: currentSearchParams });
103103
}, [orgId, currentSearchParams]);
104104

105-
// Use SWR to fetch risks with polling when onboarding is active
105+
// Use SWR to fetch risks with polling for real-time updates
106+
// Poll faster during onboarding, slower otherwise
106107
const { data: risksData } = useSWR(swrKey, fetcher, {
107108
fallbackData: { data: initialRisks, pageCount: initialPageCount },
108-
refreshInterval: isActive ? 1000 : 0, // Poll every 1 second when onboarding is active
109+
refreshInterval: isActive ? 1000 : 5000, // 1s during onboarding, 5s otherwise
109110
revalidateOnFocus: false,
110111
revalidateOnReconnect: true,
111112
keepPreviousData: true,

apps/app/src/app/(app)/[orgId]/risk/[riskId]/components/RiskActions.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import { Cog } from 'lucide-react';
2121
import { useAction } from 'next-safe-action/hooks';
2222
import { useState } from 'react';
2323
import { toast } from 'sonner';
24+
import { useSWRConfig } from 'swr';
2425

2526
export function RiskActions({ riskId }: { riskId: string }) {
27+
const { mutate: globalMutate } = useSWRConfig();
2628
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
2729

2830
// Get SWR mutate function to refresh risk data after mutations
@@ -31,8 +33,19 @@ export function RiskActions({ riskId }: { riskId: string }) {
3133
const regenerate = useAction(regenerateRiskMitigationAction, {
3234
onSuccess: () => {
3335
toast.success('Regeneration triggered. This may take a moment.');
34-
// Trigger SWR revalidation to refresh risk data
36+
// Trigger SWR revalidation for risk detail, list views, and comments
3537
refreshRisk();
38+
globalMutate(
39+
(key) => Array.isArray(key) && key[0] === 'risks',
40+
undefined,
41+
{ revalidate: true },
42+
);
43+
// Invalidate comments cache for this risk
44+
globalMutate(
45+
(key) => typeof key === 'string' && key.includes(`/v1/comments`) && key.includes(riskId),
46+
undefined,
47+
{ revalidate: true },
48+
);
3649
},
3750
onError: () => toast.error('Failed to trigger mitigation regeneration'),
3851
});

apps/app/src/app/(app)/[orgId]/vendors/(overview)/components/VendorDeleteCell.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Button } from '@comp/ui/button';
1212
import { Trash2 } from 'lucide-react';
1313
import * as React from 'react';
1414
import { toast } from 'sonner';
15+
import { useSWRConfig } from 'swr';
1516
import { deleteVendor } from '../actions/deleteVendor';
1617
import type { GetVendorsResult } from '../data/queries';
1718

@@ -22,6 +23,7 @@ interface VendorDeleteCellProps {
2223
}
2324

2425
export const VendorDeleteCell: React.FC<VendorDeleteCellProps> = ({ vendor }) => {
26+
const { mutate } = useSWRConfig();
2527
const [isRemoveAlertOpen, setIsRemoveAlertOpen] = React.useState(false);
2628
const [isDeleting, setIsDeleting] = React.useState(false);
2729

@@ -34,6 +36,12 @@ export const VendorDeleteCell: React.FC<VendorDeleteCellProps> = ({ vendor }) =>
3436
if (response?.data?.success) {
3537
toast.success(`Vendor "${vendor.name}" has been deleted.`);
3638
setIsRemoveAlertOpen(false);
39+
// Invalidate all vendors SWR caches (any key starting with 'vendors')
40+
mutate(
41+
(key) => Array.isArray(key) && key[0] === 'vendors',
42+
undefined,
43+
{ revalidate: true },
44+
);
3745
} else {
3846
toast.error(String(response?.data?.error) || 'Failed to delete vendor.');
3947
}

apps/app/src/app/(app)/[orgId]/vendors/(overview)/components/VendorsTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ export function VendorsTable({
109109
return await callGetVendorsAction({ orgId, searchParams: currentSearchParams });
110110
}, [orgId, currentSearchParams]);
111111

112-
// Use SWR to fetch vendors with polling when onboarding is active
112+
// Use SWR to fetch vendors with polling for real-time updates
113+
// Poll faster during onboarding, slower otherwise
113114
const { data: vendorsData } = useSWR(swrKey, fetcher, {
114115
fallbackData: { data: initialVendors, pageCount: initialPageCount },
115-
refreshInterval: isActive ? 1000 : 0, // Poll every 1 second when onboarding is active
116+
refreshInterval: isActive ? 1000 : 5000, // 1s during onboarding, 5s otherwise
116117
revalidateOnFocus: false,
117118
revalidateOnReconnect: true,
118119
keepPreviousData: true,

apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorActions.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import { useAction } from 'next-safe-action/hooks';
2222
import { useQueryState } from 'nuqs';
2323
import { useState } from 'react';
2424
import { toast } from 'sonner';
25+
import { useSWRConfig } from 'swr';
2526

2627
export function VendorActions({ vendorId }: { vendorId: string }) {
28+
const { mutate: globalMutate } = useSWRConfig();
2729
const [_, setOpen] = useQueryState('vendor-overview-sheet');
2830
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
2931

@@ -33,8 +35,19 @@ export function VendorActions({ vendorId }: { vendorId: string }) {
3335
const regenerate = useAction(regenerateVendorMitigationAction, {
3436
onSuccess: () => {
3537
toast.success('Regeneration triggered. This may take a moment.');
36-
// Trigger SWR revalidation to refresh vendor data
38+
// Trigger SWR revalidation for vendor detail, list views, and comments
3739
refreshVendor();
40+
globalMutate(
41+
(key) => Array.isArray(key) && key[0] === 'vendors',
42+
undefined,
43+
{ revalidate: true },
44+
);
45+
// Invalidate comments cache for this vendor
46+
globalMutate(
47+
(key) => typeof key === 'string' && key.includes(`/v1/comments`) && key.includes(vendorId),
48+
undefined,
49+
{ revalidate: true },
50+
);
3851
},
3952
onError: () => toast.error('Failed to trigger mitigation regeneration'),
4053
});

apps/app/src/app/(app)/[orgId]/vendors/components/create-vendor-form.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useQueryState } from 'nuqs';
1818
import { useState } from 'react';
1919
import { useForm } from 'react-hook-form';
2020
import { toast } from 'sonner';
21+
import { useSWRConfig } from 'swr';
2122
import { z } from 'zod';
2223
import { createVendorAction } from '../actions/create-vendor-action';
2324
import { searchGlobalVendorsAction } from '../actions/search-global-vendors-action';
@@ -42,6 +43,7 @@ export function CreateVendorForm({
4243
assignees: (Member & { user: User })[];
4344
organizationId: string;
4445
}) {
46+
const { mutate } = useSWRConfig();
4547
const [_, setCreateVendorSheet] = useQueryState('createVendorSheet');
4648

4749
const [searchQuery, setSearchQuery] = useState('');
@@ -53,6 +55,12 @@ export function CreateVendorForm({
5355
onSuccess: async () => {
5456
toast.success('Vendor created successfully');
5557
setCreateVendorSheet(null);
58+
// Invalidate all vendors SWR caches (any key starting with 'vendors')
59+
mutate(
60+
(key) => Array.isArray(key) && key[0] === 'vendors',
61+
undefined,
62+
{ revalidate: true },
63+
);
5664
},
5765
onError: () => {
5866
toast.error('Failed to create vendor');

apps/app/src/components/forms/risks/create-risk-form.tsx

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,24 @@ import { useAction } from 'next-safe-action/hooks';
1717
import { useQueryState } from 'nuqs';
1818
import { useForm } from 'react-hook-form';
1919
import { toast } from 'sonner';
20+
import { useSWRConfig } from 'swr';
2021
import type { z } from 'zod';
2122

2223
export function CreateRisk({ assignees }: { assignees: (Member & { user: User })[] }) {
23-
// Get the same query parameters as the table
24-
const [search] = useQueryState('search');
25-
const [page] = useQueryState('page', {
26-
defaultValue: 1,
27-
parse: Number.parseInt,
28-
});
29-
const [pageSize] = useQueryState('pageSize', {
30-
defaultValue: 10,
31-
parse: Number,
32-
});
33-
const [status] = useQueryState<RiskStatus | null>('status', {
34-
defaultValue: null,
35-
parse: (value) => value as RiskStatus | null,
36-
});
37-
const [department] = useQueryState<Departments | null>('department', {
38-
defaultValue: null,
39-
parse: (value) => value as Departments | null,
40-
});
41-
const [assigneeId] = useQueryState<string | null>('assigneeId', {
42-
defaultValue: null,
43-
parse: (value) => value,
44-
});
24+
const { mutate } = useSWRConfig();
4525

4626
const [_, setCreateRiskSheet] = useQueryState('create-risk-sheet');
4727

4828
const createRisk = useAction(createRiskAction, {
4929
onSuccess: async () => {
5030
toast.success('Risk created successfully');
5131
setCreateRiskSheet(null);
32+
// Invalidate all risks SWR caches (any key starting with 'risks')
33+
mutate(
34+
(key) => Array.isArray(key) && key[0] === 'risks',
35+
undefined,
36+
{ revalidate: true },
37+
);
5238
},
5339
onError: () => {
5440
toast.error('Failed to create risk');

apps/app/src/hooks/use-comments-api.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ interface UpdateCommentData {
4949
contextUrl?: string;
5050
}
5151

52+
// Default polling interval for real-time updates (5 seconds)
53+
const DEFAULT_COMMENTS_POLLING_INTERVAL = 5000;
54+
5255
/**
5356
* Generic hook to fetch comments for any entity using SWR
57+
* Includes polling for real-time updates (e.g., when trigger.dev tasks create comments)
5458
*/
5559
export function useComments(
5660
entityId: string | null,
@@ -60,7 +64,11 @@ export function useComments(
6064
const endpoint =
6165
entityId && entityType ? `/v1/comments?entityId=${entityId}&entityType=${entityType}` : null;
6266

63-
return useApiSWR<Comment[]>(endpoint, options);
67+
return useApiSWR<Comment[]>(endpoint, {
68+
...options,
69+
// Enable polling for real-time updates (when trigger.dev tasks create comments)
70+
refreshInterval: options.refreshInterval ?? DEFAULT_COMMENTS_POLLING_INTERVAL,
71+
});
6472
}
6573

6674
/**

0 commit comments

Comments
 (0)