Files
Jordi Enric 6c6ac567b1 feat(studio): workers list behind the workers flag (FE-4188) (#49193)
## What

The Workers list page at `/project/[ref]/workers`, behind
`useFlag('workers')`. Reads `GET /v2/projects/{ref}/workers`.

- Sidebar and command-menu entries, both hidden when the flag is off
- Name search, state and access filters, pagination
- Read-only

Gating, in order: flag off redirects to the project home; a 404 from the
API means the project is outside the alpha allow-list ("not enabled for
this project"); a 403 means the caller lacks the permission
(`NoPermission`); anything else is an `AlertError`.

`parseWorker` in `data/workers/workers.utils.ts` is the only place the
API shape becomes the view model. It validates with zod, so a drifted
response fails the query instead of half-rendering a row.

## How to test

Only on the **Mockamaster** project in staging — it is the one project
in the alpha allow-list, and standing a worker up anywhere else is
involved right now.

1. Staging dashboard → Mockamaster → **Compute** in the sidebar
2. Expect the `dashboard-test` worker: state `Active`, runtime Deno,
private, US West, 2 GB · 1 vCPU · 1 inst
3. Open any other project's `/workers` URL → "Compute is not enabled for
this project"
4. Turn the `workers` flag off → the sidebar entry disappears and the
URL redirects to the project home

Closes FE-4188
2026-08-20 17:54:57 +02:00

64 lines
2.4 KiB
TypeScript

import { RUNTIMES, type RuntimeMeta } from './Workers.constants'
import type { Worker, WorkerAccess, WorkerBuildState } from './Workers.types'
import { ResponseError } from '@/types'
export interface WorkerFilters {
search: string
state: WorkerBuildState | 'all'
access: WorkerAccess | 'all'
}
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.buildState === filters.state
const matchesAccess = filters.access === 'all' || worker.access === filters.access
return matchesSearch && matchesState && matchesAccess
})
}
export interface Page<T> {
items: T[]
currentPage: number
totalPages: number
startIndex: number
}
// Clamps the requested page so filtering down to fewer results never strands an empty page.
export const getPage = <T>(items: T[], requestedPage: number, pageSize: number): Page<T> => {
const totalPages = Math.max(1, Math.ceil(items.length / pageSize))
const currentPage = Math.min(Math.max(1, requestedPage), totalPages)
const startIndex = (currentPage - 1) * pageSize
return {
items: items.slice(startIndex, startIndex + pageSize),
currentPage,
totalPages,
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`
// A project outside the alpha allow-list gets a 404, not a 403.
export const isWorkersUnavailable = (error: Error | null): boolean =>
error instanceof ResponseError && error.code === 404
// An enrolled project still answers 403 when the caller lacks the workers permission.
export const isWorkersForbidden = (error: Error | null): boolean =>
error instanceof ResponseError && error.code === 403