mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): poll state to keep ui in sync (#50216)
Poll compute data to keep ui in sync - every 3s when state is transitioning - every 10s when idle ## To test - open compute - deploy compute instance via cli - check ui updates automatically while state changes (new -> active -> deleting -> removal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Compute data now refreshes automatically, with faster updates while instances are building or being deleted. * Added clearer manual refresh feedback in the compute interface. * **Improvements** * Improved compute table layout with fixed column sizing and truncated long instance names. * Region and resource columns remain responsive while maintaining consistent widths. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
9a9228a9f8
commit
d513d013c5
6 files changed
+86
-20
No files matched your search
@@ -146,15 +146,15 @@ export const ComputeList = ({
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table>
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Runtime</TableHead>
|
||||
<TableHead>Access</TableHead>
|
||||
<TableHead className="hidden xl:table-cell">Region</TableHead>
|
||||
<TableHead className="hidden lg:table-cell">Resources</TableHead>
|
||||
<TableHead className="w-36">State</TableHead>
|
||||
<TableHead className="w-40">Runtime</TableHead>
|
||||
<TableHead className="w-28">Access</TableHead>
|
||||
<TableHead className="hidden w-20 xl:table-cell">Region</TableHead>
|
||||
<TableHead className="hidden w-48 lg:table-cell">Resources</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -171,31 +171,32 @@ export const ComputeList = ({
|
||||
className="cursor-pointer"
|
||||
onClick={() => router.push(computeInstancePagePath(instance.name))}
|
||||
>
|
||||
<TableCell className="font-medium text-foreground">
|
||||
<TableCell className="max-w-0 font-medium text-foreground">
|
||||
<Link
|
||||
href={computeInstancePagePath(instance.name)}
|
||||
className="block truncate"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{instance.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="w-36">
|
||||
<ComputeInstanceStatePill instance={instance} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="w-40 truncate">
|
||||
<RuntimeBadge runtime={instance.runtime} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="w-28">
|
||||
{instance.access === 'public' ? (
|
||||
<Badge variant="success">Public</Badge>
|
||||
) : (
|
||||
<Badge>Private</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-foreground-light xl:table-cell">
|
||||
<TableCell className="hidden w-20 truncate text-foreground-light xl:table-cell">
|
||||
{COMPUTE_REGION_SHORT}
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-foreground-light lg:table-cell">
|
||||
<TableCell className="hidden w-48 truncate text-foreground-light lg:table-cell">
|
||||
{formatResources(instance)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { parseComputeInstance } from './compute.utils'
|
||||
import { computeInstanceRefetchInterval, parseComputeInstance } from './compute.utils'
|
||||
import { computeKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
@@ -32,4 +32,5 @@ export const computeInstanceQueryOptions = ({ projectRef, name }: ComputeInstanc
|
||||
queryKey: computeKeys.detail(projectRef, name),
|
||||
queryFn: ({ signal }) => getComputeInstance({ projectRef, name }, signal),
|
||||
enabled: IS_PLATFORM && typeof projectRef !== 'undefined' && typeof name !== 'undefined',
|
||||
refetchInterval: (query) => computeInstanceRefetchInterval(query.state.data),
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { parseComputeInstance } from './compute.utils'
|
||||
import { computeRefetchInterval, parseComputeInstance } from './compute.utils'
|
||||
import { computeKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
@@ -29,4 +29,5 @@ export const computeQueryOptions = ({ projectRef }: ComputeVariables) =>
|
||||
queryFn: ({ signal }) => getComputeInstances({ projectRef }, signal),
|
||||
enabled: IS_PLATFORM && typeof projectRef !== 'undefined',
|
||||
refetchOnWindowFocus: 'always',
|
||||
refetchInterval: (query) => computeRefetchInterval(query.state.data),
|
||||
})
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { parseComputeInstance } from './compute.utils'
|
||||
import {
|
||||
COMPUTE_POLL_BASELINE_INTERVAL,
|
||||
COMPUTE_POLL_TRANSIENT_INTERVAL,
|
||||
computeInstanceRefetchInterval,
|
||||
computeRefetchInterval,
|
||||
parseComputeInstance,
|
||||
} from './compute.utils'
|
||||
|
||||
const datum = (attributes: Record<string, unknown>) => ({
|
||||
id: 'embed',
|
||||
@@ -63,3 +69,38 @@ describe('parseComputeInstance', () => {
|
||||
expect(() => parseComputeInstance(undefined)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeRefetchInterval', () => {
|
||||
const active = parseComputeInstance(datum({}))
|
||||
const building = parseComputeInstance(datum({ build_state: 'building' }))
|
||||
const deleting = parseComputeInstance(datum({ deleting: true }))
|
||||
|
||||
it('keeps polling once every instance has settled', () => {
|
||||
expect(computeRefetchInterval([active])).toBe(COMPUTE_POLL_BASELINE_INTERVAL)
|
||||
expect(computeRefetchInterval([])).toBe(COMPUTE_POLL_BASELINE_INTERVAL)
|
||||
expect(computeRefetchInterval(undefined)).toBe(COMPUTE_POLL_BASELINE_INTERVAL)
|
||||
})
|
||||
|
||||
it('polls faster while any instance is building or being deleted', () => {
|
||||
expect(computeRefetchInterval([active, building])).toBe(COMPUTE_POLL_TRANSIENT_INTERVAL)
|
||||
expect(computeRefetchInterval([active, deleting])).toBe(COMPUTE_POLL_TRANSIENT_INTERVAL)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeInstanceRefetchInterval', () => {
|
||||
it('keeps polling once the instance has settled', () => {
|
||||
expect(computeInstanceRefetchInterval(parseComputeInstance(datum({})))).toBe(
|
||||
COMPUTE_POLL_BASELINE_INTERVAL
|
||||
)
|
||||
expect(computeInstanceRefetchInterval(undefined)).toBe(COMPUTE_POLL_BASELINE_INTERVAL)
|
||||
})
|
||||
|
||||
it('polls faster while the instance is building or being deleted', () => {
|
||||
expect(
|
||||
computeInstanceRefetchInterval(parseComputeInstance(datum({ build_state: 'building' })))
|
||||
).toBe(COMPUTE_POLL_TRANSIENT_INTERVAL)
|
||||
expect(computeInstanceRefetchInterval(parseComputeInstance(datum({ deleting: true })))).toBe(
|
||||
COMPUTE_POLL_TRANSIENT_INTERVAL
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -36,6 +36,22 @@ const ComputeInstanceResponseSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
// Deploys/deletes happen via the CLI, not a dashboard mutation, so polling never fully stops —
|
||||
// it just slows down once nothing is building or deleting.
|
||||
export const COMPUTE_POLL_BASELINE_INTERVAL = 10000
|
||||
export const COMPUTE_POLL_TRANSIENT_INTERVAL = 3000
|
||||
|
||||
const isTransient = (instance: ComputeInstance) =>
|
||||
instance.buildState === 'building' || instance.isDeleting
|
||||
|
||||
export const computeRefetchInterval = (instances: ComputeInstance[] | undefined) =>
|
||||
instances?.some(isTransient) ? COMPUTE_POLL_TRANSIENT_INTERVAL : COMPUTE_POLL_BASELINE_INTERVAL
|
||||
|
||||
export const computeInstanceRefetchInterval = (instance: ComputeInstance | undefined) =>
|
||||
instance !== undefined && isTransient(instance)
|
||||
? COMPUTE_POLL_TRANSIENT_INTERVAL
|
||||
: COMPUTE_POLL_BASELINE_INTERVAL
|
||||
|
||||
export const parseComputeInstance = (datum: unknown): ComputeInstance => {
|
||||
const { id, attributes } = ComputeInstanceResponseSchema.parse(datum)
|
||||
return {
|
||||
|
||||
@@ -34,16 +34,22 @@ import type { NextPageWithLayout } from '@/types'
|
||||
const ComputePage: NextPageWithLayout = () => {
|
||||
const { ref } = useParams()
|
||||
const [isDeployInstructionsOpen, setIsDeployInstructionsOpen] = useState(false)
|
||||
const [isManuallyRefreshing, setIsManuallyRefreshing] = useState(false)
|
||||
const {
|
||||
data: instances,
|
||||
error,
|
||||
isPending,
|
||||
isError,
|
||||
isSuccess,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery(computeQueryOptions({ projectRef: ref }))
|
||||
|
||||
// Separate from the query's own isFetching so background polling doesn't flash the button
|
||||
const handleManualRefresh = () => {
|
||||
setIsManuallyRefreshing(true)
|
||||
refetch().finally(() => setIsManuallyRefreshing(false))
|
||||
}
|
||||
|
||||
const isNotEnrolled = isError && isComputeUnavailable(error)
|
||||
const isMissingPermission = isError && isComputeForbidden(error)
|
||||
const isUnexpectedError = isError && !isNotEnrolled && !isMissingPermission
|
||||
@@ -88,8 +94,8 @@ const ComputePage: NextPageWithLayout = () => {
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<RefreshCw />}
|
||||
loading={isFetching}
|
||||
onClick={() => refetch()}
|
||||
loading={isManuallyRefreshing}
|
||||
onClick={handleManualRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -104,8 +110,8 @@ const ComputePage: NextPageWithLayout = () => {
|
||||
projectRef={ref}
|
||||
instances={instances}
|
||||
onDeploy={() => setIsDeployInstructionsOpen(true)}
|
||||
onRefresh={() => refetch()}
|
||||
isRefreshing={isFetching}
|
||||
onRefresh={handleManualRefresh}
|
||||
isRefreshing={isManuallyRefreshing}
|
||||
/>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
|
||||
Reference in new issue
Block a user