mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
feat(studio): read the workers list from the v2 Management API
Replaces the seeded store with GET /v2/projects/{ref}/workers, and reshapes the
view model, state pill and resource formatting to what the endpoint returns.
This commit is contained in:
1 parent
c3df56c22f
commit
fa58d0753e
11 files changed
+177
-254
No files matched your search
@@ -1,10 +1,9 @@
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { getRuntimeMeta } from './Workers.constants'
|
||||
import type { WorkerRuntime } from './Workers.types'
|
||||
import { formatRuntime, getRuntimeMeta } from './Workers.utils'
|
||||
|
||||
interface RuntimeBadgeProps {
|
||||
runtime: WorkerRuntime
|
||||
runtime: string | undefined
|
||||
className?: string
|
||||
}
|
||||
|
||||
@@ -12,8 +11,10 @@ export const RuntimeBadge = ({ runtime, className }: RuntimeBadgeProps) => {
|
||||
const meta = getRuntimeMeta(runtime)
|
||||
return (
|
||||
<span className={cn('inline-flex items-center gap-2 text-sm text-foreground-light', className)}>
|
||||
<span className={cn('h-2.5 w-2.5 rounded-[3px]', meta.swatchClassName)} />
|
||||
{meta.label}
|
||||
<span
|
||||
className={cn('h-2.5 w-2.5 rounded-[3px]', meta?.swatchClassName ?? 'bg-foreground-muted')}
|
||||
/>
|
||||
{formatRuntime(runtime)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { WORKER_STATE_META } from './Workers.constants'
|
||||
import type { WorkerState } from './Workers.types'
|
||||
import { getWorkerStateMeta } from './Workers.constants'
|
||||
import type { Worker } from './Workers.types'
|
||||
|
||||
interface WorkerStatePillProps {
|
||||
state: WorkerState
|
||||
worker: Worker
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const WorkerStatePill = ({ state, className }: WorkerStatePillProps) => {
|
||||
const meta = WORKER_STATE_META[state]
|
||||
const isPulsing = state === 'deploying' || state === 'draining' || state === 'resuming'
|
||||
export const WorkerStatePill = ({ worker, className }: WorkerStatePillProps) => {
|
||||
const meta = getWorkerStateMeta(worker)
|
||||
const isPulsing = worker.buildState === 'building' || worker.isDeleting
|
||||
|
||||
return (
|
||||
<span className={cn('inline-flex items-center gap-2', className)}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkerRuntime, WorkerSize, WorkerState } from './Workers.types'
|
||||
import type { Worker, WorkerBuildState } from './Workers.types'
|
||||
|
||||
// Locked to one region at alpha, so nothing in the UI lets you change it.
|
||||
export const WORKERS_REGION = 'us-west-1'
|
||||
@@ -8,127 +8,74 @@ export const WORKERS_REGION_SHORT = 'US West'
|
||||
// Private workers share this URL shape; access is enforced by the gateway, not the path.
|
||||
export const workerGatewayUrl = (name: string) => `https://workers.supabase.co/v1/${name}`
|
||||
|
||||
export const LISTENING_PORT = 8080
|
||||
|
||||
export interface RuntimeMeta {
|
||||
value: WorkerRuntime
|
||||
label: string
|
||||
cli: string
|
||||
baseImage: string
|
||||
entrypoint: string
|
||||
swatchClassName: string
|
||||
}
|
||||
|
||||
export const RUNTIMES: RuntimeMeta[] = [
|
||||
{
|
||||
value: 'node',
|
||||
// Keyed by the `spec.runtime` values the Management API returns.
|
||||
export const RUNTIMES: Record<string, RuntimeMeta> = {
|
||||
node: {
|
||||
label: 'Node.js 24',
|
||||
cli: 'node',
|
||||
baseImage: 'node:24-slim',
|
||||
entrypoint: 'node index.js',
|
||||
swatchClassName: 'bg-[#5FA04E]',
|
||||
},
|
||||
{
|
||||
value: 'deno',
|
||||
deno: {
|
||||
label: 'Deno 2',
|
||||
cli: 'deno',
|
||||
baseImage: 'denoland/deno:latest',
|
||||
entrypoint: 'deno run main.ts',
|
||||
swatchClassName: 'bg-[#70FFAF]',
|
||||
},
|
||||
{
|
||||
value: 'dockerfile',
|
||||
label: 'Dockerfile',
|
||||
cli: 'dockerfile',
|
||||
baseImage: 'From ./Dockerfile',
|
||||
entrypoint: 'CMD from Dockerfile',
|
||||
swatchClassName: 'bg-[#2496ED]',
|
||||
},
|
||||
{
|
||||
value: 'bun',
|
||||
bun: {
|
||||
label: 'Bun 1',
|
||||
cli: 'bun',
|
||||
baseImage: 'oven/bun:latest',
|
||||
entrypoint: 'bun run index.ts',
|
||||
swatchClassName: 'bg-[#FBF0DF]',
|
||||
},
|
||||
{
|
||||
value: 'python',
|
||||
python: {
|
||||
label: 'Python 3.14',
|
||||
cli: 'python',
|
||||
baseImage: 'python:3.14-slim',
|
||||
entrypoint: 'python main.py',
|
||||
swatchClassName: 'bg-[#3776AB]',
|
||||
},
|
||||
]
|
||||
|
||||
export const getRuntimeMeta = (runtime: WorkerRuntime): RuntimeMeta =>
|
||||
RUNTIMES.find((r) => r.value === runtime) ?? RUNTIMES[0]
|
||||
|
||||
export interface SizeMeta {
|
||||
value: WorkerSize
|
||||
memory: string
|
||||
vcpu: string
|
||||
label: string
|
||||
dockerfile: {
|
||||
label: 'Dockerfile',
|
||||
baseImage: 'From ./Dockerfile',
|
||||
entrypoint: 'CMD from Dockerfile',
|
||||
swatchClassName: 'bg-[#2496ED]',
|
||||
},
|
||||
}
|
||||
|
||||
export const SIZES: SizeMeta[] = [
|
||||
{ value: '2x1', memory: '2 GB', vcpu: '1 vCPU', label: '2×1 · 2 GB · 1 vCPU' },
|
||||
{ value: '4x2', memory: '4 GB', vcpu: '2 vCPU', label: '4×2 · 4 GB · 2 vCPU' },
|
||||
]
|
||||
|
||||
export const getSizeMeta = (size: WorkerSize): SizeMeta =>
|
||||
SIZES.find((s) => s.value === size) ?? SIZES[0]
|
||||
|
||||
export const formatResources = (size: WorkerSize, instances: number): string => {
|
||||
const meta = getSizeMeta(size)
|
||||
return `${meta.vcpu} · ${meta.memory} · ${instances} inst`
|
||||
}
|
||||
|
||||
export const LISTENING_PORT = 8080
|
||||
|
||||
export interface WorkerStateMeta {
|
||||
label: string
|
||||
dotClassName: string
|
||||
textClassName: string
|
||||
}
|
||||
|
||||
export const WORKER_STATE_META: Record<WorkerState, WorkerStateMeta> = {
|
||||
deploying: {
|
||||
label: 'Deploying',
|
||||
const BUILD_STATE_META: Record<WorkerBuildState, WorkerStateMeta> = {
|
||||
building: {
|
||||
label: 'Building',
|
||||
dotClassName: 'bg-warning',
|
||||
textClassName: 'text-foreground-light',
|
||||
},
|
||||
active: { label: 'Active', dotClassName: 'bg-brand', textClassName: 'text-foreground-light' },
|
||||
draining: {
|
||||
label: 'Draining',
|
||||
dotClassName: 'bg-warning',
|
||||
textClassName: 'text-foreground-light',
|
||||
},
|
||||
suspended: {
|
||||
label: 'Suspended',
|
||||
dotClassName: 'bg-foreground-muted',
|
||||
textClassName: 'text-foreground-light',
|
||||
},
|
||||
resuming: {
|
||||
label: 'Resuming',
|
||||
dotClassName: 'bg-warning',
|
||||
textClassName: 'text-foreground-light',
|
||||
},
|
||||
errored: {
|
||||
label: 'Errored',
|
||||
failed: {
|
||||
label: 'Failed',
|
||||
dotClassName: 'bg-destructive',
|
||||
textClassName: 'text-foreground-light',
|
||||
},
|
||||
killed: {
|
||||
label: 'Killed',
|
||||
dotClassName: 'bg-foreground-muted',
|
||||
textClassName: 'text-foreground-lighter',
|
||||
},
|
||||
}
|
||||
|
||||
export const WORKER_ERROR_REASON_LABEL: Record<string, string> = {
|
||||
crash: 'The worker crashed with a non-zero exit code',
|
||||
unresponsive: 'The worker did not respond on $PORT',
|
||||
build: 'The build failed before the worker could start',
|
||||
entrypoint: 'The entrypoint could not be found or executed',
|
||||
dependency: 'A dependency failed to install during build',
|
||||
const DELETING_STATE_META: WorkerStateMeta = {
|
||||
label: 'Deleting',
|
||||
dotClassName: 'bg-foreground-muted',
|
||||
textClassName: 'text-foreground-lighter',
|
||||
}
|
||||
|
||||
export const getWorkerStateMeta = (worker: Worker): WorkerStateMeta =>
|
||||
worker.isDeleting ? DELETING_STATE_META : BUILD_STATE_META[worker.buildState]
|
||||
@@ -1,53 +1,24 @@
|
||||
export type WorkerRuntime = 'node' | 'deno' | 'bun' | 'python' | 'dockerfile'
|
||||
|
||||
export type WorkerSize = '2x1' | '4x2'
|
||||
export type WorkerBuildState = 'building' | 'active' | 'failed'
|
||||
|
||||
export type WorkerAccess = 'public' | 'private'
|
||||
|
||||
// A health-check failure is `errored` with errorReason 'unresponsive', not its own state,
|
||||
// which is how infra reports it.
|
||||
export type WorkerState =
|
||||
| 'deploying'
|
||||
| 'active'
|
||||
| 'draining'
|
||||
| 'suspended'
|
||||
| 'resuming'
|
||||
| 'errored'
|
||||
| 'killed'
|
||||
|
||||
export type WorkerErrorReason = 'crash' | 'unresponsive' | 'build' | 'entrypoint' | 'dependency'
|
||||
|
||||
export type WorkerLifecycleLevel = 'info' | 'error'
|
||||
|
||||
export interface WorkerLifecycleEvent {
|
||||
id: string
|
||||
at: string
|
||||
level: WorkerLifecycleLevel
|
||||
message: string
|
||||
reason?: WorkerErrorReason
|
||||
export interface WorkerInstanceTally {
|
||||
declared: number
|
||||
live: number
|
||||
ready: number
|
||||
stale: number
|
||||
}
|
||||
|
||||
export interface Worker {
|
||||
id: string
|
||||
name: string
|
||||
runtime: WorkerRuntime
|
||||
size: WorkerSize
|
||||
buildState: WorkerBuildState
|
||||
isDeleting: boolean
|
||||
runtime?: string
|
||||
size: string
|
||||
access: WorkerAccess
|
||||
instances: number
|
||||
region: string
|
||||
state: WorkerState
|
||||
errorReason?: WorkerErrorReason
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
events: WorkerLifecycleEvent[]
|
||||
}
|
||||
|
||||
export interface WorkerMetrics {
|
||||
requests24h: number
|
||||
errors24h: number
|
||||
errorRate: number
|
||||
avgLatencyMs: number
|
||||
p99LatencyMs: number
|
||||
cpuPercent: number
|
||||
memoryPercent: number
|
||||
declaredInstances: number
|
||||
instances?: WorkerInstanceTally
|
||||
imageVersion?: string
|
||||
stateReason?: string
|
||||
instancesError?: string
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { Worker } from './Workers.types'
|
||||
import { filterWorkers, getPage } from './Workers.utils'
|
||||
import { filterWorkers, formatResources, formatRuntime, formatSize, getPage } from './Workers.utils'
|
||||
|
||||
const worker = (overrides: Partial<Worker> & Pick<Worker, 'name'>): Worker => ({
|
||||
id: overrides.name,
|
||||
buildState: 'active',
|
||||
isDeleting: false,
|
||||
runtime: 'node',
|
||||
size: '2x1',
|
||||
size: '2gb-1vcpu',
|
||||
access: 'public',
|
||||
instances: 1,
|
||||
region: 'us-west-1',
|
||||
state: 'active',
|
||||
createdAt: '2026-08-10T09:00:00.000Z',
|
||||
updatedAt: '2026-08-10T09:00:00.000Z',
|
||||
events: [],
|
||||
declaredInstances: 1,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@@ -21,9 +17,9 @@ const NO_FILTERS = { search: '', state: 'all', access: 'all' } as const
|
||||
|
||||
describe('filterWorkers', () => {
|
||||
const workers = [
|
||||
worker({ name: 'embed', state: 'active', access: 'public' }),
|
||||
worker({ name: 'resize-images', state: 'suspended', access: 'private' }),
|
||||
worker({ name: 'embed-batch', state: 'errored', access: 'public' }),
|
||||
worker({ name: 'embed', buildState: 'active', access: 'public' }),
|
||||
worker({ name: 'resize-images', buildState: 'building', access: 'private' }),
|
||||
worker({ name: 'embed-batch', buildState: 'failed', access: 'public' }),
|
||||
]
|
||||
|
||||
it('returns every worker when no filters are set', () => {
|
||||
@@ -39,8 +35,8 @@ describe('filterWorkers', () => {
|
||||
expect(filterWorkers(workers, { ...NO_FILTERS, search: ' resize ' })).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('filters by state and by access', () => {
|
||||
expect(filterWorkers(workers, { ...NO_FILTERS, state: 'errored' }).map((w) => w.name)).toEqual([
|
||||
it('filters by build state and by access', () => {
|
||||
expect(filterWorkers(workers, { ...NO_FILTERS, state: 'failed' }).map((w) => w.name)).toEqual([
|
||||
'embed-batch',
|
||||
])
|
||||
expect(filterWorkers(workers, { ...NO_FILTERS, access: 'private' }).map((w) => w.name)).toEqual(
|
||||
@@ -83,3 +79,37 @@ describe('getPage', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatSize', () => {
|
||||
it('splits the memory and vCPU parts the API reports', () => {
|
||||
expect(formatSize('2gb-1vcpu')).toBe('2 GB · 1 vCPU')
|
||||
expect(formatSize('4gb-2vcpu')).toBe('4 GB · 2 vCPU')
|
||||
})
|
||||
|
||||
it('falls back to the raw value for a shape it does not recognize', () => {
|
||||
expect(formatSize('8gb-4vcpu-gpu')).toBe('8gb-4vcpu-gpu')
|
||||
expect(formatSize('')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatRuntime', () => {
|
||||
it('labels a known runtime', () => {
|
||||
expect(formatRuntime('python')).toBe('Python 3.14')
|
||||
})
|
||||
|
||||
it('shows the raw value for a runtime it does not know', () => {
|
||||
expect(formatRuntime('rust')).toBe('rust')
|
||||
})
|
||||
|
||||
it('reports unknown when the API omits the runtime', () => {
|
||||
expect(formatRuntime(undefined)).toBe('Unknown')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatResources', () => {
|
||||
it('combines size and declared instance count', () => {
|
||||
expect(formatResources(worker({ name: 'embed', declaredInstances: 3 }))).toBe(
|
||||
'2 GB · 1 vCPU · 3 inst'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Worker, WorkerAccess, WorkerState } from './Workers.types'
|
||||
import { RUNTIMES, type RuntimeMeta } from './Workers.constants'
|
||||
import type { Worker, WorkerAccess, WorkerBuildState } from './Workers.types'
|
||||
|
||||
export interface WorkerFilters {
|
||||
search: string
|
||||
state: WorkerState | 'all'
|
||||
state: WorkerBuildState | 'all'
|
||||
access: WorkerAccess | 'all'
|
||||
}
|
||||
|
||||
@@ -10,7 +11,7 @@ export const filterWorkers = (workers: Worker[], filters: WorkerFilters): Worker
|
||||
const search = filters.search.trim().toLowerCase()
|
||||
return workers.filter((worker) => {
|
||||
const matchesSearch = worker.name.toLowerCase().includes(search)
|
||||
const matchesState = filters.state === 'all' || worker.state === filters.state
|
||||
const matchesState = filters.state === 'all' || worker.buildState === filters.state
|
||||
const matchesAccess = filters.access === 'all' || worker.access === filters.access
|
||||
return matchesSearch && matchesState && matchesAccess
|
||||
})
|
||||
@@ -35,3 +36,19 @@ export const getPage = <T>(items: T[], requestedPage: number, pageSize: number):
|
||||
startIndex,
|
||||
}
|
||||
}
|
||||
|
||||
export const getRuntimeMeta = (runtime: string | undefined): RuntimeMeta | undefined =>
|
||||
runtime === undefined ? undefined : RUNTIMES[runtime]
|
||||
|
||||
export const formatRuntime = (runtime: string | undefined): string =>
|
||||
getRuntimeMeta(runtime)?.label ?? runtime ?? 'Unknown'
|
||||
|
||||
// The API reports size as e.g. "2gb-1vcpu"; render the parts when they parse, the raw value if not.
|
||||
export const formatSize = (size: string): string => {
|
||||
const match = size.match(/^(\d+)gb-(\d+)vcpu$/)
|
||||
if (!match) return size
|
||||
return `${match[1]} GB · ${match[2]} vCPU`
|
||||
}
|
||||
|
||||
export const formatResources = (worker: Worker): string =>
|
||||
`${formatSize(worker.size)} · ${worker.declaredInstances} inst`
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
} from 'ui'
|
||||
|
||||
import { RuntimeBadge } from './RuntimeBadge'
|
||||
import { formatResources, WORKERS_REGION_SHORT } from './Workers.constants'
|
||||
import type { Worker, WorkerAccess, WorkerState } from './Workers.types'
|
||||
import { filterWorkers, getPage } from './Workers.utils'
|
||||
import { WORKERS_REGION_SHORT } from './Workers.constants'
|
||||
import type { Worker, WorkerAccess, WorkerBuildState } from './Workers.types'
|
||||
import { filterWorkers, formatResources, getPage } from './Workers.utils'
|
||||
import { WorkerStatePill } from './WorkerStatePill'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
|
||||
@@ -32,12 +32,11 @@ interface WorkersListProps {
|
||||
workers: Worker[]
|
||||
}
|
||||
|
||||
const STATE_FILTERS: { value: WorkerState | 'all'; label: string }[] = [
|
||||
const STATE_FILTERS: { value: WorkerBuildState | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: 'All states' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'deploying', label: 'Deploying' },
|
||||
{ value: 'suspended', label: 'Suspended' },
|
||||
{ value: 'errored', label: 'Errored' },
|
||||
{ value: 'building', label: 'Building' },
|
||||
{ value: 'failed', label: 'Failed' },
|
||||
]
|
||||
|
||||
const ACCESS_FILTERS: { value: WorkerAccess | 'all'; label: string }[] = [
|
||||
@@ -48,7 +47,7 @@ const ACCESS_FILTERS: { value: WorkerAccess | 'all'; label: string }[] = [
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const parseStateFilter = (value: string): WorkerState | 'all' =>
|
||||
const parseStateFilter = (value: string): WorkerBuildState | 'all' =>
|
||||
STATE_FILTERS.find((option) => option.value === value)?.value ?? 'all'
|
||||
|
||||
const parseAccessFilter = (value: string): WorkerAccess | 'all' =>
|
||||
@@ -57,7 +56,7 @@ const parseAccessFilter = (value: string): WorkerAccess | 'all' =>
|
||||
export const WorkersList = ({ projectRef, workers }: WorkersListProps) => {
|
||||
const router = useRouter()
|
||||
const [search, setSearch] = useState('')
|
||||
const [stateFilter, setStateFilter] = useState<WorkerState | 'all'>('all')
|
||||
const [stateFilter, setStateFilter] = useState<WorkerBuildState | 'all'>('all')
|
||||
const [accessFilter, setAccessFilter] = useState<WorkerAccess | 'all'>('all')
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
@@ -151,7 +150,7 @@ export const WorkersList = ({ projectRef, workers }: WorkersListProps) => {
|
||||
)}
|
||||
{paged.map((worker) => (
|
||||
<TableRow
|
||||
key={worker.id}
|
||||
key={worker.name}
|
||||
className="cursor-pointer"
|
||||
onClick={() => router.push(workerUrl(worker.name))}
|
||||
>
|
||||
@@ -161,7 +160,7 @@ export const WorkersList = ({ projectRef, workers }: WorkersListProps) => {
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkerStatePill state={worker.state} />
|
||||
<WorkerStatePill worker={worker} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RuntimeBadge runtime={worker.runtime} />
|
||||
@@ -177,7 +176,7 @@ export const WorkersList = ({ projectRef, workers }: WorkersListProps) => {
|
||||
{WORKERS_REGION_SHORT}
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-foreground-light lg:table-cell">
|
||||
{formatResources(worker.size, worker.instances)}
|
||||
{formatResources(worker)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { workersKeys } from './keys'
|
||||
import { mockWorkers } from './workers.mock'
|
||||
import { parseWorker } from './workers.utils'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import type { ResponseError } from '@/types'
|
||||
|
||||
export type WorkerVariables = { projectRef?: string; name?: string }
|
||||
export type WorkerError = ResponseError
|
||||
|
||||
// Mirrors GET /v2/projects/{ref}/workers/{name}, seeded for the same reason as the list query.
|
||||
async function getWorker({ projectRef, name }: WorkerVariables) {
|
||||
async function getWorker({ projectRef, name }: WorkerVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!name) throw new Error('name is required')
|
||||
return mockWorkers().find((worker) => worker.name === name) ?? null
|
||||
|
||||
const { data, error } = await get('/v2/projects/{ref}/workers/{name}', {
|
||||
params: { path: { ref: projectRef, name } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return parseWorker(data.data)
|
||||
}
|
||||
|
||||
export type WorkerData = Awaited<ReturnType<typeof getWorker>>
|
||||
@@ -20,6 +27,6 @@ export type WorkerData = Awaited<ReturnType<typeof getWorker>>
|
||||
export const workerQueryOptions = ({ projectRef, name }: WorkerVariables) =>
|
||||
queryOptions({
|
||||
queryKey: workersKeys.detail(projectRef, name),
|
||||
queryFn: () => getWorker({ projectRef, name }),
|
||||
queryFn: ({ signal }) => getWorker({ projectRef, name }, signal),
|
||||
enabled: IS_PLATFORM && typeof projectRef !== 'undefined' && typeof name !== 'undefined',
|
||||
})
|
||||
@@ -1,18 +1,24 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { workersKeys } from './keys'
|
||||
import { mockWorkers } from './workers.mock'
|
||||
import { parseWorker } from './workers.utils'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import type { ResponseError } from '@/types'
|
||||
|
||||
export type WorkersVariables = { projectRef?: string }
|
||||
export type WorkersError = ResponseError
|
||||
|
||||
// GET /v2/projects/{ref}/workers exists but is restricted to allowlisted projects, and its response
|
||||
// carries neither lifecycle events nor metrics. Seeded data until both are available (FUNC-774).
|
||||
async function getWorkers({ projectRef }: WorkersVariables) {
|
||||
async function getWorkers({ projectRef }: WorkersVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
return mockWorkers()
|
||||
|
||||
const { data, error } = await get('/v2/projects/{ref}/workers', {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data.data.map(parseWorker)
|
||||
}
|
||||
|
||||
export type WorkersData = Awaited<ReturnType<typeof getWorkers>>
|
||||
@@ -20,6 +26,6 @@ export type WorkersData = Awaited<ReturnType<typeof getWorkers>>
|
||||
export const workersQueryOptions = ({ projectRef }: WorkersVariables) =>
|
||||
queryOptions({
|
||||
queryKey: workersKeys.list(projectRef),
|
||||
queryFn: () => getWorkers({ projectRef }),
|
||||
queryFn: ({ signal }) => getWorkers({ projectRef }, signal),
|
||||
enabled: IS_PLATFORM && typeof projectRef !== 'undefined',
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
import { WORKERS_REGION } from '@/components/interfaces/Workers/Workers.constants'
|
||||
import type { Worker } from '@/components/interfaces/Workers/Workers.types'
|
||||
|
||||
// Timestamps derive from a fixed base rather than Date.now() so SSR and client render identically.
|
||||
const SEED_BASE = new Date('2026-08-10T09:00:00.000Z').getTime()
|
||||
const seedTime = (minutesAgo: number) => new Date(SEED_BASE - minutesAgo * 60_000).toISOString()
|
||||
|
||||
export function mockWorkers(): Worker[] {
|
||||
return [
|
||||
{
|
||||
id: 'wk-embed',
|
||||
name: 'embed',
|
||||
runtime: 'python',
|
||||
size: '4x2',
|
||||
access: 'public',
|
||||
instances: 2,
|
||||
region: WORKERS_REGION,
|
||||
state: 'active',
|
||||
createdAt: seedTime(60 * 26),
|
||||
updatedAt: seedTime(42),
|
||||
events: [
|
||||
{ id: 'ev-embed-1', at: seedTime(60 * 26), level: 'info', message: 'Deploy started' },
|
||||
{
|
||||
id: 'ev-embed-2',
|
||||
at: seedTime(60 * 26 - 1),
|
||||
level: 'info',
|
||||
message: 'Built image python:3.14-slim',
|
||||
},
|
||||
{
|
||||
id: 'ev-embed-3',
|
||||
at: seedTime(60 * 26 - 1),
|
||||
level: 'info',
|
||||
message: 'Worker active on 2 instances',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wk-resize-images',
|
||||
name: 'resize-images',
|
||||
runtime: 'node',
|
||||
size: '2x1',
|
||||
access: 'private',
|
||||
instances: 1,
|
||||
region: WORKERS_REGION,
|
||||
state: 'active',
|
||||
createdAt: seedTime(60 * 8),
|
||||
updatedAt: seedTime(15),
|
||||
events: [
|
||||
{ id: 'ev-resize-1', at: seedTime(60 * 8), level: 'info', message: 'Deploy started' },
|
||||
{
|
||||
id: 'ev-resize-2',
|
||||
at: seedTime(60 * 8 - 1),
|
||||
level: 'info',
|
||||
message: 'Worker active on 1 instance',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wk-nightly-report',
|
||||
name: 'nightly-report',
|
||||
runtime: 'deno',
|
||||
size: '2x1',
|
||||
access: 'private',
|
||||
instances: 1,
|
||||
region: WORKERS_REGION,
|
||||
state: 'suspended',
|
||||
createdAt: seedTime(60 * 72),
|
||||
updatedAt: seedTime(60 * 5),
|
||||
events: [
|
||||
{
|
||||
id: 'ev-nightly-1',
|
||||
at: seedTime(60 * 5),
|
||||
level: 'info',
|
||||
message: 'Suspended after idle timeout',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { components } from 'api-types'
|
||||
|
||||
import type { Worker, WorkerBuildState } from '@/components/interfaces/Workers/Workers.types'
|
||||
|
||||
type WorkerDatum = components['schemas']['V2WorkerResponse']['data']
|
||||
|
||||
const BUILD_STATES: WorkerBuildState[] = ['building', 'active', 'failed']
|
||||
|
||||
const parseBuildState = (value: string): WorkerBuildState =>
|
||||
BUILD_STATES.find((state) => state === value) ?? 'failed'
|
||||
|
||||
export const parseWorker = ({ id, attributes }: WorkerDatum): Worker => ({
|
||||
name: id,
|
||||
buildState: parseBuildState(attributes.build_state),
|
||||
isDeleting: attributes.deleting ?? false,
|
||||
runtime: attributes.spec.runtime,
|
||||
size: attributes.spec.size,
|
||||
access: attributes.spec.exposure === 'public' ? 'public' : 'private',
|
||||
declaredInstances: attributes.spec.instances,
|
||||
instances: attributes.instances,
|
||||
imageVersion: attributes.image_version,
|
||||
stateReason: attributes.state_reason,
|
||||
instancesError: attributes.instances_error,
|
||||
})
|
||||
Reference in new issue
Block a user