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:
Francesco Sansalvadore authored and GitHub committed 2026-09-10 17:11:49 +02:00
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),
})
+2 -1
View File
@@ -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),
})
+42 -1
View File
@@ -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
)
})
})
+16
View File
@@ -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>