From 825e3fa2c75c0b223ea7032a37bd3db2406ca3d2 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Wed, 10 Jun 2026 06:24:32 +0200 Subject: [PATCH] fix(dashboard): settle remote sync button state --- apps/dashboard/src/lib/api.ts | 2 ++ .../src/lib/project-sync-status.test.ts | 32 +++++++++++++++++++ apps/dashboard/src/lib/project-sync-status.ts | 4 +++ apps/dashboard/src/routes/index.tsx | 10 ++++-- .../src/routes/projects/$projectId.tsx | 10 ++++-- 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/src/lib/api.ts b/apps/dashboard/src/lib/api.ts index 80f42394..990b6acd 100644 --- a/apps/dashboard/src/lib/api.ts +++ b/apps/dashboard/src/lib/api.ts @@ -12,6 +12,7 @@ import { useQuery, } from '@tanstack/react-query'; +import { shouldPollRemoteStatus } from './project-sync-status'; import type { CategoriesResponse, CombineDuplicateConflict, @@ -232,6 +233,7 @@ export function remoteStatusOptions(projectId?: string) { queryKey: ['remote-status', projectId ?? ''], queryFn: () => fetchJson(url), staleTime: 5_000, + refetchInterval: (query) => (shouldPollRemoteStatus(query.state.data) ? 1_000 : false), }); } diff --git a/apps/dashboard/src/lib/project-sync-status.test.ts b/apps/dashboard/src/lib/project-sync-status.test.ts index f1b72f79..3740fc45 100644 --- a/apps/dashboard/src/lib/project-sync-status.test.ts +++ b/apps/dashboard/src/lib/project-sync-status.test.ts @@ -6,6 +6,7 @@ import { buildRemoteStatusItems, formatRemoteRunCount, getProjectSyncView, + shouldPollRemoteStatus, } from './project-sync-status'; describe('getProjectSyncView', () => { @@ -50,6 +51,25 @@ describe('getProjectSyncView', () => { }); }); + it('returns to a clean sync action when a status refetch settles an overlapping sync', () => { + const view = getProjectSyncView( + { + configured: true, + available: true, + sync_status: 'clean', + run_count: 1, + dirty_paths: [], + }, + false, + ); + + expect(view).toMatchObject({ + state: 'clean', + actionLabel: 'Sync Project', + canSync: true, + }); + }); + it('treats diverged history as a conflict-safe blocked state', () => { expect( getProjectSyncView({ @@ -154,3 +174,15 @@ describe('buildRemoteStatusItems', () => { expect(items.some((item) => item.startsWith('Last synced '))).toBe(true); }); }); + +describe('shouldPollRemoteStatus', () => { + it('polls only while the server reports an overlapping sync in progress', () => { + expect( + shouldPollRemoteStatus({ configured: true, available: true, sync_status: 'syncing' }), + ).toBe(true); + expect( + shouldPollRemoteStatus({ configured: true, available: true, sync_status: 'clean' }), + ).toBe(false); + expect(shouldPollRemoteStatus(undefined)).toBe(false); + }); +}); diff --git a/apps/dashboard/src/lib/project-sync-status.ts b/apps/dashboard/src/lib/project-sync-status.ts index a6c2a432..40ddc834 100644 --- a/apps/dashboard/src/lib/project-sync-status.ts +++ b/apps/dashboard/src/lib/project-sync-status.ts @@ -63,6 +63,10 @@ export function buildRemoteStatusItems( ].filter((item): item is string => item !== undefined); } +export function shouldPollRemoteStatus(status: RemoteStatusResponse | undefined): boolean { + return status?.sync_status === 'syncing'; +} + export function getProjectSyncView( status: RemoteStatusResponse | undefined, syncInFlight = false, diff --git a/apps/dashboard/src/routes/index.tsx b/apps/dashboard/src/routes/index.tsx index 7c7fa779..bec70db1 100644 --- a/apps/dashboard/src/routes/index.tsx +++ b/apps/dashboard/src/routes/index.tsx @@ -19,6 +19,7 @@ import { type RunSourceFilter, RunSourceToolbar } from '~/components/RunSourceTo import { TargetsTab } from '~/components/TargetsTab'; import { addProjectApi, + remoteStatusOptions, syncRemoteResultsApi, useCompare, useEvalRuns, @@ -248,17 +249,20 @@ function SingleProjectHome() { setSyncFeedback(null); try { const result = await syncRemoteResultsApi(); + queryClient.setQueryData(remoteStatusOptions().queryKey, result); setSyncFeedback(buildProjectSyncFeedback(result)); - await Promise.all([ + void Promise.all([ queryClient.invalidateQueries({ queryKey: ['runs'] }), queryClient.invalidateQueries({ queryKey: ['experiments'] }), queryClient.invalidateQueries({ queryKey: ['compare'] }), queryClient.invalidateQueries({ queryKey: ['targets'] }), queryClient.invalidateQueries({ queryKey: ['remote-status', ''] }), - ]); + ]).catch(() => undefined); } catch (err) { setSyncFeedback(buildProjectSyncErrorFeedback(err, remoteStatus)); - await queryClient.invalidateQueries({ queryKey: ['remote-status', ''] }); + void queryClient + .invalidateQueries({ queryKey: ['remote-status', ''] }) + .catch(() => undefined); } finally { setSyncInFlight(false); } diff --git a/apps/dashboard/src/routes/projects/$projectId.tsx b/apps/dashboard/src/routes/projects/$projectId.tsx index f45b4a0e..26a8b7a1 100644 --- a/apps/dashboard/src/routes/projects/$projectId.tsx +++ b/apps/dashboard/src/routes/projects/$projectId.tsx @@ -17,6 +17,7 @@ import { type RunSourceFilter, RunSourceToolbar } from '~/components/RunSourceTo import { TargetsTab } from '~/components/TargetsTab'; import { projectCompareOptions, + remoteStatusOptions, syncRemoteResultsApi, useEvalRuns, useInfiniteProjectRunList, @@ -150,18 +151,21 @@ function ProjectRunsTab({ setSyncFeedback(null); try { const result = await syncRemoteResultsApi(projectId); + queryClient.setQueryData(remoteStatusOptions(projectId).queryKey, result); const feedback = buildProjectSyncFeedback(result); setSyncFeedback(feedback); - await Promise.all([ + void Promise.all([ queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'runs'] }), queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'experiments'] }), queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'compare'] }), queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'targets'] }), queryClient.invalidateQueries({ queryKey: ['remote-status', projectId] }), - ]); + ]).catch(() => undefined); } catch (err) { setSyncFeedback(buildProjectSyncErrorFeedback(err, remoteStatus)); - await queryClient.invalidateQueries({ queryKey: ['remote-status', projectId] }); + void queryClient + .invalidateQueries({ queryKey: ['remote-status', projectId] }) + .catch(() => undefined); } finally { setSyncInFlight(false); }