Skip to content

Commit e897b11

Browse files
committed
fix(heureka): fetches always fresh data in RemediationHistoryPanel after mutation
1 parent e9bc877 commit e897b11

3 files changed

Lines changed: 30 additions & 18 deletions

File tree

apps/heureka/src/api/fetchRemediations.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ import { RouteContext } from "../routes/-types"
99

1010
type FetchRemediationsParams = Pick<RouteContext, "queryClient" | "apiClient"> & {
1111
filter?: RemediationFilter
12+
staleTime?: number
1213
}
1314

1415
export const fetchRemediations = ({
1516
queryClient,
1617
apiClient,
1718
filter,
19+
staleTime = 2.5 * 60 * 1000,
1820
}: FetchRemediationsParams): Promise<ObservableQuery.Result<GetRemediationsQuery>> => {
1921
const queryKey = ["remediations", filter]
2022

2123
return queryClient.ensureQueryData({
2224
queryKey,
23-
staleTime: 2.5 * 60 * 1000,
25+
staleTime,
2426
queryFn: () =>
2527
apiClient.query<GetRemediationsQuery>({
2628
query: GetRemediationsDocument,

apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/index.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type RemediationHistoryPanelProps = {
3737
onClose: () => void
3838
/** Called after a successful revert so the parent can refetch getRemediations and getImages. */
3939
onRevertSuccess?: (vulnerability: string) => void | Promise<void>
40+
/** Increment to force a fresh fetch of remediations (e.g. after createRemediation or deleteRemediation). */
41+
refreshKey?: number
4042
}
4143

4244
const COLUMN_SPAN = 6
@@ -115,23 +117,25 @@ export const RemediationHistoryPanel = ({
115117
vulnerability,
116118
onClose,
117119
onRevertSuccess,
120+
refreshKey,
118121
}: RemediationHistoryPanelProps) => {
119122
const { apiClient, queryClient } = useRouteContext({ from: "/services/$service" })
120123
const [revertMessage, setRevertMessage] = useTimedState<RevertMessage>(10000, (m) => m.variant === "success")
121124

122125
const remediationsPromise = useMemo(() => {
123126
if (!vulnerability) return null
124127

125-
return fetchRemediations({
126-
apiClient,
127-
queryClient,
128-
filter: {
129-
service: [service],
130-
image: [image],
131-
vulnerability: [vulnerability],
132-
},
133-
})
134-
}, [service, image, vulnerability, apiClient, queryClient])
128+
const filter = {
129+
service: [service],
130+
image: [image],
131+
vulnerability: [vulnerability],
132+
}
133+
134+
// Always fetch from scratch — the panel opens on user interaction and must show current state.
135+
// staleTime: 0 makes ensureQueryData treat any cached entry as immediately stale, forcing a
136+
// network request without cancelling in-flight queries (unlike removeQueries).
137+
return fetchRemediations({ apiClient, queryClient, filter, staleTime: 0 })
138+
}, [service, image, vulnerability, apiClient, queryClient, refreshKey])
135139

136140
const handleRevert = async (remediationId: string) => {
137141
// Clear any existing feedback when starting a new revert operation.

apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/index.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const RemediatedVulnerabilitiesTabContent = ({
127127
onDataRefresh,
128128
selectedVulnerability,
129129
onSelectVulnerability,
130+
refreshKey,
130131
}: {
131132
service: string
132133
image: string
@@ -137,6 +138,7 @@ const RemediatedVulnerabilitiesTabContent = ({
137138
onDataRefresh?: (vulnerability: string) => void | Promise<void>
138139
selectedVulnerability: string | null
139140
onSelectVulnerability: (cve: string | null) => void
141+
refreshKey: number
140142
}) => {
141143
return (
142144
<>
@@ -194,6 +196,7 @@ const RemediatedVulnerabilitiesTabContent = ({
194196
vulnerability={selectedVulnerability}
195197
onClose={() => onSelectVulnerability(null)}
196198
onRevertSuccess={onDataRefresh}
199+
refreshKey={refreshKey}
197200
/>
198201
</>
199202
)
@@ -237,7 +240,7 @@ export const ImageIssuesList = ({
237240
)
238241
const [vulnerabilitiesSuccessMessage, setVulnerabilitiesSuccessMessage] = useTimedState<string>(10000)
239242
const [pollErrorMessage, setPollErrorMessage] = useTimedState<string>(10000)
240-
const [, setRefreshKey] = useState(0)
243+
const [refreshKey, setRefreshKey] = useState(0)
241244
const pollTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([])
242245
const isMountedRef = useRef(true)
243246

@@ -251,6 +254,7 @@ export const ImageIssuesList = ({
251254

252255
const refreshIssuesData = useCallback(
253256
async (vulnerability: string) => {
257+
// Helper: match only queries for the currently viewed service + image.
254258
const matchesCurrentServiceAndImage = (
255259
filter: { service?: string[]; image?: string[]; repository?: string[]; vulnerability?: string[] } | undefined
256260
) =>
@@ -259,6 +263,7 @@ export const ImageIssuesList = ({
259263
((Array.isArray(filter?.image) && filter.image.includes(image.repository)) ||
260264
(Array.isArray(filter?.repository) && filter.repository.includes(image.repository)))
261265

266+
// 1. Immediately refetch remediations for the affected CVE so the panel shows fresh data.
262267
await queryClient.refetchQueries({
263268
type: "all",
264269
predicate: (query) => {
@@ -275,6 +280,7 @@ export const ImageIssuesList = ({
275280
},
276281
})
277282

283+
// 2. Immediately refetch images so the active/remediated tabs reflect the change.
278284
await queryClient.refetchQueries({
279285
type: "all",
280286
predicate: (query) => {
@@ -286,14 +292,13 @@ export const ImageIssuesList = ({
286292
},
287293
})
288294

295+
// 3. Bump refreshKey so memoized promises (e.g. RemediationHistoryPanel) re-read from cache.
289296
setRefreshKey((k) => k + 1)
290297

291-
// Cancel any pending polls from a previous operation and schedule fresh ones.
292-
// The backend takes ~5–6 min to propagate changes, so we poll at 2.5 and 5 min.
293-
// Note: we intentionally use setTimeout + invalidateQueries here instead of
294-
// React Query's refetchInterval. The entire data layer in this app uses
295-
// queryClient.ensureQueryData() + React use() for Suspense-based data fetching,
296-
// not useQuery hooks.
298+
// 4. Schedule background re-polls at 2.5 min and 5 min.
299+
// The backend takes ~5–6 min to propagate, so a second pass catches late updates.
300+
// Note: we use setTimeout + invalidateQueries (not refetchInterval) because the data
301+
// layer relies on ensureQueryData + React use() for Suspense, not useQuery hooks.
297302
pollTimeoutsRef.current.forEach(clearTimeout)
298303
pollTimeoutsRef.current = []
299304
;[2.5 * 60 * 1000, 5 * 60 * 1000].forEach((delay) => {
@@ -417,6 +422,7 @@ export const ImageIssuesList = ({
417422
onDataRefresh={refreshIssuesData}
418423
selectedVulnerability={vulRemediations ?? null}
419424
onSelectVulnerability={handleRemediationPanelVulnerabilityChange}
425+
refreshKey={refreshKey}
420426
/>
421427
</TabPanel>
422428
</Tabs>

0 commit comments

Comments
 (0)