From abb7f3ede25600b1f0ce3a5d2a2982d5456043fe Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:42:57 +0200 Subject: [PATCH] fix(workers): refresh Workers view FE-4323 (#49887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The Workers view can remain stale after a worker is deployed through the CLI, because the dashboard has no deployment mutation to invalidate its list query. ## Fix Add a manual Refresh action to the Workers header and force the Workers list query to refetch whenever the browser regains focus. ## How to test - Open a project’s Workers view and select Refresh. - Expected result: the list requests current worker data and renders it. - Deploy a worker through the CLI, then return focus to the Workers view. - Expected result: the Workers list refreshes even when its cached data is fresh. Closes FE-4323. ## Summary by CodeRabbit - **New Features** - Added Refresh buttons to the Workers page and worker list. - Refreshing displays the latest worker information and shows a loading state while data is retrieved. - Worker data now automatically refreshes when the browser window regains focus. - Added a Refresh action to unexpected-error messages, allowing failed requests to be retried without leaving the page. - **Bug Fixes** - Improved recovery from failed worker data requests through in-page retry support. --- .../interfaces/Workers/WorkersList.test.tsx | 23 ++++++++++--- .../interfaces/Workers/WorkersList.tsx | 18 +++++++--- apps/studio/data/workers/workers-query.ts | 1 + .../pages/project/[ref]/workers/index.tsx | 23 ++++++++++++- .../project/[ref]/workers/index.test.tsx | 34 ++++++++++++++++++- 5 files changed, 88 insertions(+), 11 deletions(-) diff --git a/apps/studio/components/interfaces/Workers/WorkersList.test.tsx b/apps/studio/components/interfaces/Workers/WorkersList.test.tsx index e78acd3b302..09bd808ad2b 100644 --- a/apps/studio/components/interfaces/Workers/WorkersList.test.tsx +++ b/apps/studio/components/interfaces/Workers/WorkersList.test.tsx @@ -18,8 +18,16 @@ const worker = (name: string, overrides: Partial = {}): Worker => ({ ...overrides, }) -const renderList = (workers: Worker[]) => - customRender() +const renderList = (workers: Worker[], onRefresh = vi.fn()) => + customRender( + + ) const rowNames = () => screen @@ -48,12 +56,19 @@ describe('WorkersList', () => { await userEvent.type(screen.getByPlaceholderText('Search by name'), 'resize') expect(rowNames()).toEqual(['resize-images']) - expect(screen.getByText('1 worker')).toBeVisible() - await userEvent.type(screen.getByPlaceholderText('Search by name'), '-nope') expect(screen.getByText('No workers match your filters')).toBeVisible() }) + it('refreshes the workers list on request', async () => { + const onRefresh = vi.fn() + renderList([worker('embed')], onRefresh) + + await userEvent.click(screen.getByRole('button', { name: 'Refresh' })) + + expect(onRefresh).toHaveBeenCalledOnce() + }) + it('pages through the workers ten at a time', async () => { const workers = Array.from({ length: 12 }, (_, index) => worker(`worker-${index}`)) renderList(workers) diff --git a/apps/studio/components/interfaces/Workers/WorkersList.tsx b/apps/studio/components/interfaces/Workers/WorkersList.tsx index 7503d8c1533..ca2aa4b9d46 100644 --- a/apps/studio/components/interfaces/Workers/WorkersList.tsx +++ b/apps/studio/components/interfaces/Workers/WorkersList.tsx @@ -1,4 +1,4 @@ -import { ChevronLeft, ChevronRight, Terminal } from 'lucide-react' +import { ChevronLeft, ChevronRight, RefreshCw, Terminal } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' @@ -32,6 +32,8 @@ interface WorkersListProps { projectRef: string workers: Worker[] onDeploy: () => void + onRefresh: () => void + isRefreshing: boolean } const STATE_FILTERS: { value: WorkerBuildState | 'all'; label: string }[] = [ @@ -55,7 +57,13 @@ const parseStateFilter = (value: string): WorkerBuildState | 'all' => const parseAccessFilter = (value: string): WorkerAccess | 'all' => ACCESS_FILTERS.find((option) => option.value === value)?.value ?? 'all' -export const WorkersList = ({ projectRef, workers, onDeploy }: WorkersListProps) => { +export const WorkersList = ({ + projectRef, + workers, + onDeploy, + onRefresh, + isRefreshing, +}: WorkersListProps) => { const router = useRouter() const [search, setSearch] = useState('') const [stateFilter, setStateFilter] = useState('all') @@ -124,9 +132,9 @@ export const WorkersList = ({ projectRef, workers, onDeploy }: WorkersListProps)
- - {filtered.length} worker{filtered.length === 1 ? '' : 's'} - + diff --git a/apps/studio/data/workers/workers-query.ts b/apps/studio/data/workers/workers-query.ts index 5aaed1734df..a74453ffa63 100644 --- a/apps/studio/data/workers/workers-query.ts +++ b/apps/studio/data/workers/workers-query.ts @@ -28,4 +28,5 @@ export const workersQueryOptions = ({ projectRef }: WorkersVariables) => queryKey: workersKeys.list(projectRef), queryFn: ({ signal }) => getWorkers({ projectRef }, signal), enabled: IS_PLATFORM && typeof projectRef !== 'undefined', + refetchOnWindowFocus: 'always', }) diff --git a/apps/studio/pages/project/[ref]/workers/index.tsx b/apps/studio/pages/project/[ref]/workers/index.tsx index 73debf88890..327dfd8e732 100644 --- a/apps/studio/pages/project/[ref]/workers/index.tsx +++ b/apps/studio/pages/project/[ref]/workers/index.tsx @@ -1,6 +1,8 @@ import { useQuery } from '@tanstack/react-query' import { useParams } from 'common' +import { RefreshCw } from 'lucide-react' import { useState } from 'react' +import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { PageContainer } from 'ui-patterns/PageContainer' import { @@ -38,6 +40,8 @@ const WorkersPage: NextPageWithLayout = () => { isPending, isError, isSuccess, + isFetching, + refetch, } = useQuery(workersQueryOptions({ projectRef: ref })) const isNotEnrolled = isError && isWorkersUnavailable(error) @@ -74,7 +78,22 @@ const WorkersPage: NextPageWithLayout = () => { /> )} {isMissingPermission && } - {isUnexpectedError && } + {isUnexpectedError && ( + } + loading={isFetching} + onClick={() => refetch()} + > + Refresh + + } + /> + )} {isSuccess && workers.length === 0 && ( setIsDeployInstructionsOpen(true)} /> )} @@ -83,6 +102,8 @@ const WorkersPage: NextPageWithLayout = () => { projectRef={ref} workers={workers} onDeploy={() => setIsDeployInstructionsOpen(true)} + onRefresh={() => refetch()} + isRefreshing={isFetching} /> )} diff --git a/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx b/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx index a30ff8ba80b..72111ae36b9 100644 --- a/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx +++ b/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx @@ -1,5 +1,5 @@ import { QueryClient } from '@tanstack/react-query' -import { screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' import type { components } from 'api-types' import { HttpResponse } from 'msw' import { beforeEach, describe, expect, it } from 'vitest' @@ -90,6 +90,17 @@ describe('/project/[ref]/workers', () => { expect(screen.queryByRole('table')).not.toBeInTheDocument() }) + it('refreshes the workers list on request', async () => { + mockWorkersList([workerDatum('existing')]) + + await renderWorkersPage() + + mockWorkersList([workerDatum('embed')]) + fireEvent.click(screen.getByRole('button', { name: 'Refresh' })) + + expect(await screen.findByRole('link', { name: 'embed' })).toBeVisible() + }) + it('explains that a project outside the alpha is not enrolled', async () => { mockWorkersListFailure(404) @@ -109,4 +120,25 @@ describe('/project/[ref]/workers', () => { ).toBeVisible() expect(screen.queryByRole('table')).not.toBeInTheDocument() }) + + it('allows retrying an unexpected error', async () => { + let requestCount = 0 + addAPIMock({ + method: 'get', + path: '/v2/projects/:ref/workers', + response: (): HttpResponse | HttpResponse => { + if (requestCount++ === 0) { + return HttpResponse.json({ message: 'Unavailable' }, { status: 500 }) + } + + return HttpResponse.json({ data: [workerDatum('embed')] }) + }, + }) + + await renderWorkersPage() + + fireEvent.click(screen.getByRole('button', { name: 'Refresh' })) + + expect(await screen.findByRole('link', { name: 'embed' })).toBeVisible() + }) })