mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(workers): refresh Workers view FE-4323 (#49887)
## 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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
47fbf26e4b
commit
abb7f3ede2
5 files changed
+88
-11
No files matched your search
@@ -18,8 +18,16 @@ const worker = (name: string, overrides: Partial<Worker> = {}): Worker => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const renderList = (workers: Worker[]) =>
|
||||
customRender(<WorkersList projectRef="default" workers={workers} onDeploy={vi.fn()} />)
|
||||
const renderList = (workers: Worker[], onRefresh = vi.fn()) =>
|
||||
customRender(
|
||||
<WorkersList
|
||||
projectRef="default"
|
||||
workers={workers}
|
||||
onDeploy={vi.fn()}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={false}
|
||||
/>
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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<WorkerBuildState | 'all'>('all')
|
||||
@@ -124,9 +132,9 @@ export const WorkersList = ({ projectRef, workers, onDeploy }: WorkersListProps)
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center gap-3 md:ml-auto">
|
||||
<span className="text-sm text-foreground-lighter">
|
||||
{filtered.length} worker{filtered.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
<Button variant="default" icon={<RefreshCw />} loading={isRefreshing} onClick={onRefresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="primary" icon={<Terminal />} onClick={onDeploy}>
|
||||
Deploy a worker
|
||||
</Button>
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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 && <NoPermission resourceText="view this project's workers" />}
|
||||
{isUnexpectedError && <AlertError error={error} subject="Failed to retrieve workers" />}
|
||||
{isUnexpectedError && (
|
||||
<AlertError
|
||||
error={error}
|
||||
subject="Failed to retrieve workers"
|
||||
additionalActions={
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<RefreshCw />}
|
||||
loading={isFetching}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isSuccess && workers.length === 0 && (
|
||||
<WorkersEmptyState onDeploy={() => setIsDeployInstructionsOpen(true)} />
|
||||
)}
|
||||
@@ -83,6 +102,8 @@ const WorkersPage: NextPageWithLayout = () => {
|
||||
projectRef={ref}
|
||||
workers={workers}
|
||||
onDeploy={() => setIsDeployInstructionsOpen(true)}
|
||||
onRefresh={() => refetch()}
|
||||
isRefreshing={isFetching}
|
||||
/>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
|
||||
@@ -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<APIErrorBody> | HttpResponse<ListWorkersResponse> => {
|
||||
if (requestCount++ === 0) {
|
||||
return HttpResponse.json<APIErrorBody>({ message: 'Unavailable' }, { status: 500 })
|
||||
}
|
||||
|
||||
return HttpResponse.json<ListWorkersResponse>({ data: [workerDatum('embed')] })
|
||||
},
|
||||
})
|
||||
|
||||
await renderWorkersPage()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
|
||||
|
||||
expect(await screen.findByRole('link', { name: 'embed' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user