chore(studio): update product naming (#50208)

workers -> compute

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added the Compute experience for deploying, viewing, managing, and
monitoring compute instances.
- Added Compute navigation, instance detail pages, secrets, logs,
deployment dialogs, generated snippets, and CLI commands.
- Added filtering, status, availability, and data-loading support for
compute instances.

- **Updates**
- Updated labels, icons, links, feature controls, unified logs, and
secret-deletion messaging to use Compute terminology.
  - Compute routes now replace the previous Workers routes and pages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Francesco Sansalvadore authored and GitHub committed 2026-09-10 16:26:48 +02:00
1 parent 3635ab15ff
commit 0bf22ee6fc
69 files changed
+1003 -910

No files matched your search

+5 -5
View File
@@ -93,7 +93,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] `routes/project/$ref/branches.tsx` — BranchLayout only. **Delta vs plan:** the per-page `PageLayout` (with different titles + primary/secondary actions) stays in each leaf. Hoisted `BranchesPageWrapper` and `MergeRequestsPageWrapper` to top-level exports in their respective `pages/...` files so the route files can import + re-use the same wrapping.
- [x] `routes/project/$ref/logs.tsx` — LogsLayout (reads `logsLayoutTitle` from leaf staticData). Honours `skipLogsLayout: true` for `logs/index` (page handles its own ProjectLayout-wrapped content for the UnifiedLogs / no-permission cases). Refactored `pages/.../logs/index.tsx` to move the inline `<DefaultLayout>` into `getLayout` so it isn't duplicated when the TanStack project shell already provides DefaultLayout.
- [x] `routes/project/$ref/observability.tsx` — ObservabilityLayout (reads `observabilityLayoutTitle` from leaf staticData)
- [x] `routes/project/$ref/workers.tsx` — WorkersLayout (reads `workersLayoutTitle` from leaf `staticData`). Flag-gated: `WorkersLayout` itself redirects to the project home when `useFlag('workers')` is off, so the shell needs no extra guard.
- [x] `routes/project/$ref/compute.tsx` — ComputeLayout (reads `computeLayoutTitle` from leaf `staticData`). Flag-gated: `ComputeLayout` itself redirects to the project home when `useFlag('compute')` is off, so the shell needs no extra guard.
- [x] `routes/project/$ref/advisors.tsx` — AdvisorsLayout (reads `advisorsLayoutTitle` from leaf staticData). Honours `skipAdvisorsLayout: true` opt-out for the rules sub-shell, which provides its own AdvisorsLayout-less-DefaultLayout wrap. Scans whole match chain (same pattern as functions.tsx).
- [x] `routes/project/$ref/advisors/rules.tsx` — sub-shell that inlines the inner body of `AdvisorRulesLayout` (AdvisorsLayout + PageLayout with title/tabs/feature-preview badge), minus the outer DefaultLayout (already provided by the parent project shell). Sets `skipAdvisorsLayout: true` on its own staticData. **Delta vs plan:** the existing `AdvisorRulesLayout` component wraps in DefaultLayout + AdvisorsLayout internally, so reusing it as-is would double-wrap both. Inlined the inner part; the Next-side component is untouched.
- [x] `routes/project/$ref/settings.tsx` — SettingsLayout (reads `settingsLayoutTitle` from leaf staticData). Honours `skipSettingsLayout: true` for `settings/api` (redirect-only page). Adds a sub-shell at `routes/project/$ref/settings/api-keys.tsx` providing `ApiKeysLayout` for both api-keys leaves; `jwt/index` wraps in `JWTKeysLayout` inline since `jwt/legacy` doesn't share it.
@@ -229,11 +229,11 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/realtime/policies.tsx` ← `pages/project/[ref]/realtime/policies.tsx`
- [x] A `routes/project/$ref/realtime/settings.tsx` ← `pages/project/[ref]/realtime/settings.tsx`
### Project shell — `/workers/*`
### Project shell — `/compute/*`
- [x] A `routes/project/$ref/workers/index.tsx` ← `pages/project/[ref]/workers/index.tsx`
- [x] A `routes/project/$ref/workers/$name.tsx` ← `pages/project/[ref]/workers/[name].tsx`
- [x] A `routes/project/$ref/workers/secrets.tsx` ← `pages/project/[ref]/workers/secrets.tsx`
- [x] A `routes/project/$ref/compute/index.tsx` ← `pages/project/[ref]/compute/index.tsx`
- [x] A `routes/project/$ref/compute/$name.tsx` ← `pages/project/[ref]/compute/[name].tsx`
- [x] A `routes/project/$ref/compute/secrets.tsx` ← `pages/project/[ref]/compute/secrets.tsx`
### Project shell — `/functions/*`
@@ -1,14 +1,14 @@
import type { Worker, WorkerBuildState } from './Workers.types'
import type { ComputeInstance, ComputeInstanceBuildState } from './Compute.types'
// Locked to one region at alpha, so nothing in the UI lets you change it.
export const WORKERS_REGION = 'us-west-2'
export const WORKERS_REGION_LABEL = 'US West (Oregon)'
export const WORKERS_REGION_SHORT = 'US West'
export const COMPUTE_REGION = 'us-west-2'
export const COMPUTE_REGION_LABEL = 'US West (Oregon)'
export const COMPUTE_REGION_SHORT = 'US West'
// Sizes are fixed at deploy time — matches the `size` values the API accepts.
export const WORKER_SIZES = ['2gb-1vcpu', '4gb-2vcpu'] as const
export const COMPUTE_INSTANCE_SIZES = ['2gb-1vcpu', '4gb-2vcpu'] as const
export const WORKER_NAME_WORDS = [
export const COMPUTE_INSTANCE_NAME_WORDS = [
'swift',
'nimble',
'brisk',
@@ -61,7 +61,7 @@ export const WORKER_NAME_WORDS = [
'stellar',
] as const
export const workerUrl = ({
export const computeInstanceUrl = ({
endpoint,
protocol = 'https',
name,
@@ -69,7 +69,7 @@ export const workerUrl = ({
endpoint: string | undefined
protocol?: string
name: string
}) => (endpoint ? `${protocol}://${endpoint}/workers/v1/${name}` : undefined)
}) => (endpoint ? `${protocol}://${endpoint}/compute/v1/${name}` : undefined)
export const LISTENING_PORT = 8080
@@ -114,15 +114,15 @@ export const RUNTIMES: Record<string, RuntimeMeta> = {
},
}
export const WORKER_DEPLOYABLE_RUNTIMES = ['node', 'deno', 'dockerfile'] as const
export const COMPUTE_INSTANCE_DEPLOYABLE_RUNTIMES = ['node', 'deno', 'dockerfile'] as const
interface WorkerStateMeta {
interface ComputeInstanceStateMeta {
label: string
dotClassName: string
textClassName: string
}
const BUILD_STATE_META: Record<WorkerBuildState, WorkerStateMeta> = {
const BUILD_STATE_META: Record<ComputeInstanceBuildState, ComputeInstanceStateMeta> = {
building: {
label: 'Building',
dotClassName: 'bg-warning',
@@ -136,11 +136,11 @@ const BUILD_STATE_META: Record<WorkerBuildState, WorkerStateMeta> = {
},
}
const DELETING_STATE_META: WorkerStateMeta = {
const DELETING_STATE_META: ComputeInstanceStateMeta = {
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]
export const getComputeInstanceStateMeta = (instance: ComputeInstance): ComputeInstanceStateMeta =>
instance.isDeleting ? DELETING_STATE_META : BUILD_STATE_META[instance.buildState]
@@ -0,0 +1,25 @@
import type { components } from 'api-types'
type ComputeInstanceAttributes =
components['schemas']['V2WorkerResponse_Output']['data']['attributes']
export type ComputeInstanceBuildState = ComputeInstanceAttributes['build_state']
export type ComputeInstanceTally = NonNullable<ComputeInstanceAttributes['instances']>
// The API types `spec.exposure` as a free-form string; the UI only handles these two values.
export type ComputeInstanceAccess = 'public' | 'private'
export interface ComputeInstance {
name: string
buildState: ComputeInstanceBuildState
isDeleting: boolean
runtime?: string
size: string
access: ComputeInstanceAccess
declaredInstances: number
instances?: ComputeInstanceTally
imageVersion?: string
stateReason?: string
instancesError?: string
}
@@ -1,20 +1,26 @@
import { describe, expect, it } from 'vitest'
import { getWorkerStateMeta, WORKER_NAME_WORDS, workerUrl } from './Workers.constants'
import type { Worker } from './Workers.types'
import {
filterWorkers,
COMPUTE_INSTANCE_NAME_WORDS,
computeInstanceUrl,
getComputeInstanceStateMeta,
} from './Compute.constants'
import type { ComputeInstance } from './Compute.types'
import {
filterComputeInstances,
formatResources,
formatRuntime,
formatSize,
generateWorkerName,
generateComputeInstanceName,
getPage,
isWorkersForbidden,
isWorkersUnavailable,
} from './Workers.utils'
isComputeForbidden,
isComputeUnavailable,
} from './Compute.utils'
import { ResponseError } from '@/types'
const worker = (overrides: Partial<Worker> & Pick<Worker, 'name'>): Worker => ({
const instance = (
overrides: Partial<ComputeInstance> & Pick<ComputeInstance, 'name'>
): ComputeInstance => ({
buildState: 'active',
isDeleting: false,
runtime: 'node',
@@ -26,39 +32,43 @@ const worker = (overrides: Partial<Worker> & Pick<Worker, 'name'>): Worker => ({
const NO_FILTERS = { search: '', state: 'all', access: 'all' } as const
describe('filterWorkers', () => {
const workers = [
worker({ name: 'embed', buildState: 'active', access: 'public' }),
worker({ name: 'resize-images', buildState: 'building', access: 'private' }),
worker({ name: 'embed-batch', buildState: 'failed', access: 'public' }),
describe('filterComputeInstances', () => {
const instances = [
instance({ name: 'embed', buildState: 'active', access: 'public' }),
instance({ name: 'resize-images', buildState: 'building', access: 'private' }),
instance({ name: 'embed-batch', buildState: 'failed', access: 'public' }),
]
it('returns every worker when no filters are set', () => {
expect(filterWorkers(workers, NO_FILTERS)).toHaveLength(3)
it('returns every instance when no filters are set', () => {
expect(filterComputeInstances(instances, NO_FILTERS)).toHaveLength(3)
})
it('matches names case-insensitively on a partial term', () => {
const names = filterWorkers(workers, { ...NO_FILTERS, search: 'EMBED' }).map((w) => w.name)
const names = filterComputeInstances(instances, { ...NO_FILTERS, search: 'EMBED' }).map(
(i) => i.name
)
expect(names).toEqual(['embed', 'embed-batch'])
})
it('ignores surrounding whitespace in the search term', () => {
expect(filterWorkers(workers, { ...NO_FILTERS, search: ' resize ' })).toHaveLength(1)
})
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(
['resize-images']
expect(filterComputeInstances(instances, { ...NO_FILTERS, search: ' resize ' })).toHaveLength(
1
)
})
it('filters by build state and by access', () => {
expect(
filterComputeInstances(instances, { ...NO_FILTERS, state: 'failed' }).map((i) => i.name)
).toEqual(['embed-batch'])
expect(
filterComputeInstances(instances, { ...NO_FILTERS, access: 'private' }).map((i) => i.name)
).toEqual(['resize-images'])
})
it('combines filters', () => {
expect(filterWorkers(workers, { search: 'embed', state: 'active', access: 'public' })).toEqual([
workers[0],
])
expect(
filterComputeInstances(instances, { search: 'embed', state: 'active', access: 'public' })
).toEqual([instances[0]])
})
})
@@ -119,74 +129,81 @@ describe('formatRuntime', () => {
describe('formatResources', () => {
it('combines size and declared instance count', () => {
expect(formatResources(worker({ name: 'embed', declaredInstances: 3 }))).toBe(
expect(formatResources(instance({ name: 'embed', declaredInstances: 3 }))).toBe(
'2 GB · 1 vCPU · 3 inst'
)
})
})
describe('workers error classification', () => {
describe('compute error classification', () => {
const responseError = (code?: number) => new ResponseError('Denied', code)
it('reads a 404 as the project being outside the alpha allow-list', () => {
expect(isWorkersUnavailable(responseError(404))).toBe(true)
expect(isWorkersForbidden(responseError(404))).toBe(false)
expect(isComputeUnavailable(responseError(404))).toBe(true)
expect(isComputeForbidden(responseError(404))).toBe(false)
})
it('reads a 403 as a missing workers permission, not a missing enrollment', () => {
expect(isWorkersForbidden(responseError(403))).toBe(true)
expect(isWorkersUnavailable(responseError(403))).toBe(false)
it('reads a 403 as a missing compute permission, not a missing enrollment', () => {
expect(isComputeForbidden(responseError(403))).toBe(true)
expect(isComputeUnavailable(responseError(403))).toBe(false)
})
it('leaves other failures to the generic error UI', () => {
for (const error of [responseError(500), responseError(undefined), new Error('boom'), null]) {
expect(isWorkersUnavailable(error)).toBe(false)
expect(isWorkersForbidden(error)).toBe(false)
expect(isComputeUnavailable(error)).toBe(false)
expect(isComputeForbidden(error)).toBe(false)
}
})
})
describe('workerUrl', () => {
it('answers on the project domain alongside /functions/v1', () => {
expect(workerUrl({ endpoint: 'abcdefgh.supabase.co', name: 'embed' })).toBe(
'https://abcdefgh.supabase.co/workers/v1/embed'
describe('computeInstanceUrl', () => {
it('answers on the project domain alongside /compute/v1', () => {
expect(computeInstanceUrl({ endpoint: 'abcdefgh.supabase.co', name: 'embed' })).toBe(
'https://abcdefgh.supabase.co/compute/v1/embed'
)
})
it('honors a non-https protocol', () => {
expect(workerUrl({ endpoint: 'localhost:8000', protocol: 'http', name: 'embed' })).toBe(
'http://localhost:8000/workers/v1/embed'
)
expect(
computeInstanceUrl({ endpoint: 'localhost:8000', protocol: 'http', name: 'embed' })
).toBe('http://localhost:8000/compute/v1/embed')
})
it('has no url until the project settings resolve', () => {
expect(workerUrl({ endpoint: undefined, name: 'embed' })).toBeUndefined()
expect(computeInstanceUrl({ endpoint: undefined, name: 'embed' })).toBeUndefined()
})
})
describe('generateWorkerName', () => {
describe('generateComputeInstanceName', () => {
it('produces a name that already passes the CLI naming rules', () => {
const name = generateWorkerName()
expect(name).toMatch(/^worker-[a-z]+-\d{6}$/)
expect(WORKER_NAME_WORDS).toContain(name.split('-')[1])
const name = generateComputeInstanceName()
expect(name).toMatch(/^compute-[a-z]+-\d{6}$/)
expect(COMPUTE_INSTANCE_NAME_WORDS).toContain(name.split('-')[1])
})
it('varies across calls', () => {
const names = new Set(Array.from({ length: 20 }, () => generateWorkerName()))
const names = new Set(Array.from({ length: 20 }, () => generateComputeInstanceName()))
expect(names.size).toBeGreaterThan(1)
})
})
describe('getWorkerStateMeta', () => {
describe('getComputeInstanceStateMeta', () => {
it('labels every build state', () => {
expect(getWorkerStateMeta(worker({ name: 'a', buildState: 'building' })).label).toBe('Building')
expect(getWorkerStateMeta(worker({ name: 'a', buildState: 'active' })).label).toBe('Active')
expect(getWorkerStateMeta(worker({ name: 'a', buildState: 'failed' })).label).toBe('Failed')
expect(getComputeInstanceStateMeta(instance({ name: 'a', buildState: 'building' })).label).toBe(
'Building'
)
expect(getComputeInstanceStateMeta(instance({ name: 'a', buildState: 'active' })).label).toBe(
'Active'
)
expect(getComputeInstanceStateMeta(instance({ name: 'a', buildState: 'failed' })).label).toBe(
'Failed'
)
})
it('reports deleting over the build state', () => {
expect(
getWorkerStateMeta(worker({ name: 'a', buildState: 'active', isDeleting: true })).label
getComputeInstanceStateMeta(instance({ name: 'a', buildState: 'active', isDeleting: true }))
.label
).toBe('Deleting')
})
})
@@ -1,19 +1,26 @@
import { RUNTIMES, WORKER_NAME_WORDS, type RuntimeMeta } from './Workers.constants'
import type { Worker, WorkerAccess, WorkerBuildState } from './Workers.types'
import { COMPUTE_INSTANCE_NAME_WORDS, RUNTIMES, type RuntimeMeta } from './Compute.constants'
import type {
ComputeInstance,
ComputeInstanceAccess,
ComputeInstanceBuildState,
} from './Compute.types'
import { ResponseError } from '@/types'
export interface WorkerFilters {
export interface ComputeInstanceFilters {
search: string
state: WorkerBuildState | 'all'
access: WorkerAccess | 'all'
state: ComputeInstanceBuildState | 'all'
access: ComputeInstanceAccess | 'all'
}
export const filterWorkers = (workers: Worker[], filters: WorkerFilters): Worker[] => {
export const filterComputeInstances = (
instances: ComputeInstance[],
filters: ComputeInstanceFilters
): ComputeInstance[] => {
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 instances.filter((instance) => {
const matchesSearch = instance.name.toLowerCase().includes(search)
const matchesState = filters.state === 'all' || instance.buildState === filters.state
const matchesAccess = filters.access === 'all' || instance.access === filters.access
return matchesSearch && matchesState && matchesAccess
})
}
@@ -51,20 +58,21 @@ export const formatSize = (size: string): string => {
return `${match[1]} GB · ${match[2]} vCPU`
}
export const formatResources = (worker: Worker): string =>
`${formatSize(worker.size)} · ${worker.declaredInstances} inst`
export const formatResources = (instance: ComputeInstance): string =>
`${formatSize(instance.size)} · ${instance.declaredInstances} inst`
// Suggests a friendly, already-valid starting name so the deploy dialog isn't blank.
export const generateWorkerName = (): string => {
const word = WORKER_NAME_WORDS[Math.floor(Math.random() * WORKER_NAME_WORDS.length)]
export const generateComputeInstanceName = (): string => {
const word =
COMPUTE_INSTANCE_NAME_WORDS[Math.floor(Math.random() * COMPUTE_INSTANCE_NAME_WORDS.length)]
const number = Math.floor(Math.random() * 900000) + 100000
return `worker-${word}-${number}`
return `compute-${word}-${number}`
}
// A project outside the alpha allow-list gets a 404, not a 403.
export const isWorkersUnavailable = (error: Error | null): boolean =>
export const isComputeUnavailable = (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 =>
// An enrolled project still answers 403 when the caller lacks the compute permission.
export const isComputeForbidden = (error: Error | null): boolean =>
error instanceof ResponseError && error.code === 403
@@ -6,35 +6,38 @@ import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
interface WorkersEmptyStateProps {
interface ComputeEmptyStateProps {
onDeploy: () => void
}
export const WorkersEmptyState = ({ onDeploy }: WorkersEmptyStateProps) => {
const { can: canDeployWorkers } = useAsyncCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*')
export const ComputeEmptyState = ({ onDeploy }: ComputeEmptyStateProps) => {
const { can: canDeployInstances } = useAsyncCheckPermissions(
PermissionAction.FUNCTIONS_WRITE,
'*'
)
return (
<EmptyStatePresentational
icon={BoxPlus}
title="Deploy your first worker"
description="Spin up and deploy a worker locally, then deploy it to the cloud. Dockerfile, Node.js and Deno supported at Private Alpha."
title="Deploy your first Compute instance"
description="Spin up a Compute instance locally, then deploy it to the cloud. Dockerfile, Node.js and Deno supported at Private Alpha."
>
<ButtonTooltip
variant="primary"
size="tiny"
icon={<Plus size={14} />}
disabled={!canDeployWorkers}
disabled={!canDeployInstances}
onClick={onDeploy}
tooltip={{
content: {
side: 'bottom',
text: canDeployWorkers
text: canDeployInstances
? undefined
: 'You need additional permissions to deploy workers',
: 'You need additional permissions to deploy compute instances',
},
}}
>
Deploy a worker
Deploy
</ButtonTooltip>
</EmptyStatePresentational>
)
@@ -1,11 +1,14 @@
import CopyButton from '@/components/ui/CopyButton'
interface WorkerCommandLineProps {
interface ComputeInstanceCommandLineProps {
command: string
comment?: string
}
export const WorkerCommandLine = ({ command, comment }: WorkerCommandLineProps) => (
export const ComputeInstanceCommandLine = ({
command,
comment,
}: ComputeInstanceCommandLineProps) => (
<div className="space-y-1">
{comment && <p className="font-mono text-xs text-foreground-lighter">{`> ${comment}`}</p>}
<div className="flex items-center gap-2 font-mono text-sm text-foreground">
@@ -26,60 +26,60 @@ import {
import { PageNav } from 'ui-patterns/PageNav'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ComputeInstanceStatePill } from '../ComputeInstanceStatePill'
import { RuntimeBadge } from '../RuntimeBadge'
import { WorkerStatePill } from '../WorkerStatePill'
import { WorkerLogsTab } from './WorkerLogsTab'
import { WorkerOverviewTab } from './WorkerOverviewTab'
import { ComputeInstanceLogsTab } from './ComputeInstanceLogsTab'
import { ComputeInstanceOverviewTab } from './ComputeInstanceOverviewTab'
import { AlertError } from '@/components/ui/AlertError'
import type { WorkerLogStream } from '@/data/workers/worker-logs-query'
import { workerQueryOptions } from '@/data/workers/worker-query'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import type { ComputeInstanceLogStream } from '@/data/compute/compute-instance-logs-query'
import { computeInstanceQueryOptions } from '@/data/compute/compute-instance-query'
import { PRODUCT_NAME } from '@/lib/constants/compute'
type WorkerTab = 'overview' | 'invocations' | 'logs' | 'activity'
const WORKER_TABS: WorkerTab[] = ['overview', 'invocations', 'logs', 'activity']
type ComputeInstanceTab = 'overview' | 'invocations' | 'logs' | 'activity'
const COMPUTE_INSTANCE_TABS: ComputeInstanceTab[] = ['overview', 'invocations', 'logs', 'activity']
const TAB_LABEL: Record<WorkerTab, string> = {
const TAB_LABEL: Record<ComputeInstanceTab, string> = {
overview: 'Overview',
invocations: 'Invocations',
logs: 'Logs',
activity: 'Activity',
}
const TAB_STREAM: Partial<Record<WorkerTab, WorkerLogStream>> = {
const TAB_STREAM: Partial<Record<ComputeInstanceTab, ComputeInstanceLogStream>> = {
invocations: 'requests',
logs: 'output',
activity: 'builds',
}
export const WorkerDetail = () => {
const { ref: projectRef, name: workerName } = useParams()
export const ComputeInstanceDetail = () => {
const { ref: projectRef, name: instanceName } = useParams()
const [tab, setTab] = useQueryState(
'tab',
parseAsStringEnum<WorkerTab>(WORKER_TABS)
parseAsStringEnum<ComputeInstanceTab>(COMPUTE_INSTANCE_TABS)
.withDefault('overview')
.withOptions({ history: 'push' })
)
const {
data: worker,
data: instance,
error,
isPending,
isError,
} = useQuery(workerQueryOptions({ projectRef, name: workerName }))
} = useQuery(computeInstanceQueryOptions({ projectRef, name: instanceName }))
if (!projectRef) return null
if (isPending) return <GenericSkeletonLoader className="p-6" />
if (isError) return <AlertError error={error} subject="Failed to retrieve worker" />
if (isError) return <AlertError error={error} subject="Failed to retrieve instance" />
if (!worker) {
if (!instance) {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-3 py-24 text-center">
<p className="text-sm text-foreground-light">
Worker "{workerName}" does not exist in this project
Instance "{instanceName}" does not exist in this project
</p>
<Button asChild variant="default">
<Link href={`/project/${projectRef}/workers`}>Back to {PRODUCT_NAME}</Link>
<Link href={`/project/${projectRef}/compute`}>Back to {PRODUCT_NAME}</Link>
</Button>
</div>
)
@@ -93,12 +93,12 @@ export const WorkerDetail = () => {
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/project/${projectRef}/workers`}>{PRODUCT_NAME}</Link>
<Link href={`/project/${projectRef}/compute`}>{PRODUCT_NAME}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{worker.name}</BreadcrumbPage>
<BreadcrumbPage>{instance.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
@@ -111,14 +111,14 @@ export const WorkerDetail = () => {
</Card>
</PageHeaderIcon>
<PageHeaderSummary>
<PageHeaderTitle>{worker.name}</PageHeaderTitle>
<PageHeaderTitle>{instance.name}</PageHeaderTitle>
<PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 text-sm">
<WorkerStatePill worker={worker} />
<RuntimeBadge runtime={worker.runtime} />
{worker.imageVersion !== undefined && (
<ComputeInstanceStatePill instance={instance} />
<RuntimeBadge runtime={instance.runtime} />
{instance.imageVersion !== undefined && (
<span className="flex items-center gap-2 text-foreground-light">
<Package size={14} strokeWidth={1.5} className="text-foreground-lighter" />
Version {worker.imageVersion}
Version {instance.imageVersion}
</span>
)}
</PageHeaderDescription>
@@ -127,7 +127,7 @@ export const WorkerDetail = () => {
</PageHeader>
<PageNav>
<NavMenu>
{WORKER_TABS.map((item) => (
{COMPUTE_INSTANCE_TABS.map((item) => (
<NavMenuItem key={item} active={tab === item}>
<button type="button" tabIndex={0} onClick={() => setTab(item)}>
{TAB_LABEL[item]}
@@ -137,10 +137,10 @@ export const WorkerDetail = () => {
</NavMenu>
</PageNav>
{tab === 'overview' && <WorkerOverviewTab worker={worker} />}
{tab === 'overview' && <ComputeInstanceOverviewTab instance={instance} />}
{stream !== undefined && (
<div className="flex flex-1 flex-col min-h-0">
<WorkerLogsTab key={stream} workerName={worker.name} stream={stream} />
<ComputeInstanceLogsTab key={stream} instanceName={instance.name} stream={stream} />
</div>
)}
</div>
@@ -5,8 +5,8 @@ import { useState } from 'react'
import { Button, InputGroup, InputGroupAddon, InputGroupInput } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { WorkerCommandLine } from '../WorkerCommandLine'
import { WorkersLogsColumnRender } from '@/components/interfaces/Settings/Logs/LogColumnRenderers/WorkersLogsColumnRender'
import { ComputeInstanceCommandLine } from '../ComputeInstanceCommandLine'
import { ComputeLogsColumnRender } from '@/components/interfaces/Settings/Logs/LogColumnRenderers/ComputeLogsColumnRender'
import { EXPLORER_DATEPICKER_HELPERS } from '@/components/interfaces/Settings/Logs/Logs.constants'
import {
LogsDatePicker,
@@ -16,16 +16,16 @@ import type { LogData } from '@/components/interfaces/Settings/Logs/Logs.types'
import { LogTable } from '@/components/interfaces/Settings/Logs/LogTable'
import { AlertError } from '@/components/ui/AlertError'
import {
WORKER_LOG_STREAM_LABEL,
workerLogsQueryOptions,
type WorkerLogStream,
} from '@/data/workers/worker-logs-query'
COMPUTE_INSTANCE_LOG_STREAM_LABEL,
computeInstanceLogsQueryOptions,
type ComputeInstanceLogStream,
} from '@/data/compute/compute-instance-logs-query'
import { useDebouncedValue } from '@/hooks/misc/useDebouncedValue'
import { CLI_NAME } from '@/lib/constants/workers'
import { CLI_NAME } from '@/lib/constants/compute'
interface WorkerLogsTabProps {
workerName: string
stream: WorkerLogStream
interface ComputeInstanceLogsTabProps {
instanceName: string
stream: ComputeInstanceLogStream
}
const defaultDateRange = (): DatePickerValue => {
@@ -39,7 +39,7 @@ const defaultDateRange = (): DatePickerValue => {
}
}
export const WorkerLogsTab = ({ workerName, stream }: WorkerLogsTabProps) => {
export const ComputeInstanceLogsTab = ({ instanceName, stream }: ComputeInstanceLogsTabProps) => {
const { ref: projectRef } = useParams()
const [selectedLog, setSelectedLog] = useState<LogData | null>(null)
const [dateRange, setDateRange] = useState<DatePickerValue>(defaultDateRange)
@@ -54,9 +54,9 @@ export const WorkerLogsTab = ({ workerName, stream }: WorkerLogsTabProps) => {
isFetching,
refetch,
} = useQuery(
workerLogsQueryOptions({
computeInstanceLogsQueryOptions({
projectRef,
name: workerName,
name: instanceName,
stream,
iso_timestamp_start: dateRange.from,
iso_timestamp_end: dateRange.to,
@@ -64,7 +64,7 @@ export const WorkerLogsTab = ({ workerName, stream }: WorkerLogsTabProps) => {
})
)
const label = WORKER_LOG_STREAM_LABEL[stream].toLowerCase()
const label = COMPUTE_INSTANCE_LOG_STREAM_LABEL[stream].toLowerCase()
return (
<div className="flex flex-1 flex-col min-h-0">
@@ -101,7 +101,7 @@ export const WorkerLogsTab = ({ workerName, stream }: WorkerLogsTabProps) => {
{isError && (
<div className="p-4">
<AlertError error={error} subject="Failed to retrieve worker logs" />
<AlertError error={error} subject="Failed to retrieve instance logs" />
</div>
)}
@@ -115,7 +115,7 @@ export const WorkerLogsTab = ({ workerName, stream }: WorkerLogsTabProps) => {
<div className="relative flex flex-1 flex-col grow overflow-auto">
<LogTable
projectRef={projectRef ?? ''}
columnRenderers={WorkersLogsColumnRender}
columnRenderers={ComputeLogsColumnRender}
data={logs ?? []}
isLoading={isPending}
showHeader={false}
@@ -129,7 +129,9 @@ export const WorkerLogsTab = ({ workerName, stream }: WorkerLogsTabProps) => {
Follow them from the Supabase CLI while you wait for traffic.
</p>
<div className="pt-1 text-left">
<WorkerCommandLine command={`supabase ${CLI_NAME} logs ${workerName} --follow`} />
<ComputeInstanceCommandLine
command={`supabase ${CLI_NAME} logs ${instanceName} --follow`}
/>
</div>
</div>
}
@@ -12,20 +12,23 @@ import {
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { COMPUTE_REGION_LABEL, LISTENING_PORT } from '../Compute.constants'
import type { ComputeInstance } from '../Compute.types'
import { formatSize, getRuntimeMeta } from '../Compute.utils'
import { ComputeInstanceCommandLine } from '../ComputeInstanceCommandLine'
import { buildComputeInstanceCliCommands } from '../computeInstanceSnippets'
import {
COMPUTE_INSTANCE_CALL_TABS,
ComputeInstanceSnippetTabs,
} from '../ComputeInstanceSnippetTabs'
import { RuntimeBadge } from '../RuntimeBadge'
import { WorkerCommandLine } from '../WorkerCommandLine'
import { LISTENING_PORT, WORKERS_REGION_LABEL } from '../Workers.constants'
import type { Worker } from '../Workers.types'
import { formatSize, getRuntimeMeta } from '../Workers.utils'
import { buildWorkerCliCommands } from '../workerSnippets'
import { WORKER_CALL_TABS, WorkerSnippetTabs } from '../WorkerSnippetTabs'
import { CLI_NAME } from '@/lib/constants/workers'
import { CLI_NAME } from '@/lib/constants/compute'
interface WorkerOverviewTabProps {
worker: Worker
interface ComputeInstanceOverviewTabProps {
instance: ComputeInstance
}
const InstanceCount = ({
const ComputeInstanceCount = ({
label,
value,
tooltip,
@@ -64,21 +67,21 @@ const SettingsRow = ({
</div>
)
export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
const runtime = getRuntimeMeta(worker.runtime)
const commands = buildWorkerCliCommands(worker.name)
export const ComputeInstanceOverviewTab = ({ instance }: ComputeInstanceOverviewTabProps) => {
const runtime = getRuntimeMeta(instance.runtime)
const commands = buildComputeInstanceCliCommands(instance.name)
return (
<PageContainer size="small">
{worker.buildState === 'failed' && (
{instance.buildState === 'failed' && (
<PageSection>
<PageSectionContent>
<Admonition type="destructive" title="This worker failed to build">
<Admonition type="destructive" title="This instance failed to build">
<div className="space-y-3">
<p>{worker.stateReason ?? 'The build did not complete.'}</p>
<WorkerCommandLine
<p>{instance.stateReason ?? 'The build did not complete.'}</p>
<ComputeInstanceCommandLine
comment="Redeploy after fixing the build"
command={`supabase ${CLI_NAME} push ${worker.name}`}
command={`supabase ${CLI_NAME} push ${instance.name}`}
/>
</div>
</Admonition>
@@ -86,11 +89,11 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
</PageSection>
)}
{worker.instancesError !== undefined && (
{instance.instancesError !== undefined && (
<PageSection>
<PageSectionContent>
<Admonition type="warning" title="Instances reported an error">
{worker.instancesError}
{instance.instancesError}
</Admonition>
</PageSectionContent>
</PageSection>
@@ -103,36 +106,36 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
{worker.instances === undefined ? (
{instance.instances === undefined ? (
<p className="text-sm text-foreground-light">
No instances are running for this worker yet.
No instances are running for this instance yet.
</p>
) : (
<div className="space-y-3">
<p className="text-sm text-foreground-light">
<span className="tabular-nums text-foreground">{worker.instances.ready}</span> of{' '}
<span className="tabular-nums text-foreground">{worker.instances.declared}</span>{' '}
<span className="tabular-nums text-foreground">{instance.instances.ready}</span> of{' '}
<span className="tabular-nums text-foreground">{instance.instances.declared}</span>{' '}
instances ready
</p>
<div className="grid grid-cols-2 divide-x divide-y rounded-md border border-default bg-surface-100 sm:grid-cols-4 sm:divide-y-0">
<InstanceCount
<ComputeInstanceCount
label="Instances"
value={worker.instances.declared}
tooltip="The number of instances you configured for this worker."
value={instance.instances.declared}
tooltip="The number of instances you configured for this deployment."
/>
<InstanceCount
<ComputeInstanceCount
label="Live"
value={worker.instances.live}
value={instance.instances.live}
tooltip="Instances currently running."
/>
<InstanceCount
<ComputeInstanceCount
label="Ready"
value={worker.instances.ready}
value={instance.instances.ready}
tooltip="Instances passing health checks and serving requests."
/>
<InstanceCount
<ComputeInstanceCount
label="Stale"
value={worker.instances.stale}
value={instance.instances.stale}
tooltip="Instances from a previous deployment, being replaced."
/>
</div>
@@ -146,19 +149,19 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
<PageSectionSummary>
<PageSectionTitle>Container</PageSectionTitle>
<PageSectionDescription>
The runtime image and entrypoint resolved for this worker.
The runtime image and entrypoint resolved for this instance.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="rounded-md border border-default bg-surface-100">
<SettingsRow label="Runtime" isFirst>
<RuntimeBadge runtime={worker.runtime} />
<RuntimeBadge runtime={instance.runtime} />
</SettingsRow>
{worker.imageVersion !== undefined && (
{instance.imageVersion !== undefined && (
<SettingsRow label="Version">
<span className="font-mono text-xs text-foreground-light">
{worker.imageVersion}
{instance.imageVersion}
</span>
</SettingsRow>
)}
@@ -192,11 +195,11 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
<PageSectionContent>
<div className="rounded-md border border-default bg-surface-100">
<SettingsRow label="Size" isFirst>
{formatSize(worker.size)}
{formatSize(instance.size)}
</SettingsRow>
<SettingsRow label="Instances">{worker.declaredInstances}</SettingsRow>
<SettingsRow label="Instances">{instance.declaredInstances}</SettingsRow>
<SettingsRow label="Access">
{worker.access === 'public' ? (
{instance.access === 'public' ? (
<Badge variant="success">Public</Badge>
) : (
<Badge>Private</Badge>
@@ -204,7 +207,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
</SettingsRow>
<SettingsRow label="Region">
<span className="text-foreground-light">
{WORKERS_REGION_LABEL} <span className="text-foreground-lighter">(locked)</span>
{COMPUTE_REGION_LABEL} <span className="text-foreground-lighter">(locked)</span>
</span>
</SettingsRow>
</div>
@@ -215,19 +218,19 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>How to call</PageSectionTitle>
<PageSectionDescription>Call the worker over its gateway URL.</PageSectionDescription>
<PageSectionDescription>Call the instance over its gateway URL.</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<WorkerSnippetTabs
<ComputeInstanceSnippetTabs
input={{
name: worker.name,
runtime: worker.runtime,
size: worker.size,
access: worker.access,
instances: worker.declaredInstances,
name: instance.name,
runtime: instance.runtime,
size: instance.size,
access: instance.access,
instances: instance.declaredInstances,
}}
tabs={WORKER_CALL_TABS}
tabs={COMPUTE_INSTANCE_CALL_TABS}
/>
</PageSectionContent>
</PageSection>
@@ -237,14 +240,14 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
<PageSectionSummary>
<PageSectionTitle>Develop locally</PageSectionTitle>
<PageSectionDescription>
Manage this worker from the Supabase CLI.
Manage this instance from the Supabase CLI.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="space-y-4 rounded-md border border-default bg-surface-100 p-4">
{commands.map((command) => (
<WorkerCommandLine
<ComputeInstanceCommandLine
key={command.command}
comment={command.comment}
command={command.command}
@@ -3,19 +3,22 @@ import { FileCode, Sparkles, Terminal, type LucideIcon } from 'lucide-react'
import { useState } from 'react'
import { cn } from 'ui'
import { buildWorkerSnippets, type WorkerSnippetInput } from './workerSnippets'
import {
buildComputeInstanceSnippets,
type ComputeInstanceSnippetInput,
} from './computeInstanceSnippets'
import CopyButton from '@/components/ui/CopyButton'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
export type WorkerSnippetTab = 'ai' | 'config' | 'cli' | 'curl' | 'js' | 'python'
export type ComputeInstanceSnippetTab = 'ai' | 'config' | 'cli' | 'curl' | 'js' | 'python'
export const WORKER_CALL_TABS = [
export const COMPUTE_INSTANCE_CALL_TABS = [
'curl',
'js',
'python',
] as const satisfies readonly WorkerSnippetTab[]
] as const satisfies readonly ComputeInstanceSnippetTab[]
const TAB_LABEL: Record<WorkerSnippetTab, string> = {
const TAB_LABEL: Record<ComputeInstanceSnippetTab, string> = {
ai: 'AI Prompt',
config: 'config.toml',
cli: 'CLI',
@@ -24,7 +27,7 @@ const TAB_LABEL: Record<WorkerSnippetTab, string> = {
python: 'Python',
}
const TAB_ICON: Record<WorkerSnippetTab, LucideIcon> = {
const TAB_ICON: Record<ComputeInstanceSnippetTab, LucideIcon> = {
ai: Sparkles,
config: FileCode,
cli: Terminal,
@@ -33,23 +36,27 @@ const TAB_ICON: Record<WorkerSnippetTab, LucideIcon> = {
python: FileCode,
}
interface WorkerSnippetTabsProps {
input: Omit<WorkerSnippetInput, 'endpoint' | 'protocol'>
tabs?: readonly [WorkerSnippetTab, ...WorkerSnippetTab[]]
interface ComputeInstanceSnippetTabsProps {
input: Omit<ComputeInstanceSnippetInput, 'endpoint' | 'protocol'>
tabs?: readonly [ComputeInstanceSnippetTab, ...ComputeInstanceSnippetTab[]]
className?: string
}
export const WorkerSnippetTabs = ({ input, tabs = ['cli'], className }: WorkerSnippetTabsProps) => {
export const ComputeInstanceSnippetTabs = ({
input,
tabs = ['cli'],
className,
}: ComputeInstanceSnippetTabsProps) => {
const { ref } = useParams()
const [active, setActive] = useState<WorkerSnippetTab>(tabs[0])
const [active, setActive] = useState<ComputeInstanceSnippetTab>(tabs[0])
const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }, { enabled: !!ref })
const snippets = buildWorkerSnippets({
const snippets = buildComputeInstanceSnippets({
...input,
endpoint: settings?.app_config?.endpoint,
protocol: settings?.app_config?.protocol,
})
const snippetByTab: Record<WorkerSnippetTab, string> = {
const snippetByTab: Record<ComputeInstanceSnippetTab, string> = {
ai: snippets.aiPrompt,
config: snippets.configToml,
cli: snippets.cli,
@@ -1,16 +1,19 @@
import { cn } from 'ui'
import { getWorkerStateMeta } from './Workers.constants'
import type { Worker } from './Workers.types'
import { getComputeInstanceStateMeta } from './Compute.constants'
import type { ComputeInstance } from './Compute.types'
interface WorkerStatePillProps {
worker: Worker
interface ComputeInstanceStatePillProps {
instance: ComputeInstance
className?: string
}
export const WorkerStatePill = ({ worker, className }: WorkerStatePillProps) => {
const meta = getWorkerStateMeta(worker)
const isPulsing = worker.buildState === 'building' || worker.isDeleting
export const ComputeInstanceStatePill = ({
instance,
className,
}: ComputeInstanceStatePillProps) => {
const meta = getComputeInstanceStateMeta(instance)
const isPulsing = instance.buildState === 'building' || instance.isDeleting
return (
<span className={cn('inline-flex items-center gap-2', className)}>
@@ -2,12 +2,12 @@ import { screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Worker } from './Workers.types'
import { WorkersList } from './WorkersList'
import type { ComputeInstance } from './Compute.types'
import { ComputeList } from './ComputeList'
import { customRender } from '@/tests/lib/custom-render'
import { routerMock } from '@/tests/lib/route-mock'
const worker = (name: string, overrides: Partial<Worker> = {}): Worker => ({
const instance = (name: string, overrides: Partial<ComputeInstance> = {}): ComputeInstance => ({
name,
buildState: 'active',
isDeleting: false,
@@ -18,11 +18,11 @@ const worker = (name: string, overrides: Partial<Worker> = {}): Worker => ({
...overrides,
})
const renderList = (workers: Worker[], onRefresh = vi.fn()) =>
const renderList = (instances: ComputeInstance[], onRefresh = vi.fn()) =>
customRender(
<WorkersList
<ComputeList
projectRef="default"
workers={workers}
instances={instances}
onDeploy={vi.fn()}
onRefresh={onRefresh}
isRefreshing={false}
@@ -35,43 +35,43 @@ const rowNames = () =>
.slice(1)
.map((row) => within(row).getAllByRole('cell')[0].textContent)
describe('WorkersList', () => {
describe('ComputeList', () => {
beforeEach(() => {
routerMock.setCurrentUrl('/project/default/workers')
routerMock.setCurrentUrl('/project/default/compute')
})
it('renders a row per worker, linking to its detail page', () => {
renderList([worker('embed'), worker('resize', { buildState: 'building' })])
it('renders a row per instance, linking to its detail page', () => {
renderList([instance('embed'), instance('resize', { buildState: 'building' })])
expect(rowNames()).toEqual(['embed', 'resize'])
expect(screen.getByRole('link', { name: 'embed' })).toHaveAttribute(
'href',
'/project/default/workers/embed'
'/project/default/compute/embed'
)
expect(screen.getByText('Building')).toBeVisible()
})
it('narrows the rows as you search, and says so when nothing matches', async () => {
renderList([worker('embed'), worker('resize-images')])
renderList([instance('embed'), instance('resize-images')])
await userEvent.type(screen.getByPlaceholderText('Search by name'), 'resize')
expect(rowNames()).toEqual(['resize-images'])
await userEvent.type(screen.getByPlaceholderText('Search by name'), '-nope')
expect(screen.getByText('No workers match your filters')).toBeVisible()
expect(screen.getByText('No instances match your filters')).toBeVisible()
})
it('refreshes the workers list on request', async () => {
it('refreshes the instances list on request', async () => {
const onRefresh = vi.fn()
renderList([worker('embed')], onRefresh)
renderList([instance('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)
it('pages through the instances ten at a time', async () => {
const instances = Array.from({ length: 12 }, (_, index) => instance(`instance-${index}`))
renderList(instances)
expect(rowNames()).toHaveLength(10)
expect(screen.getByText('Page 1 of 2')).toBeVisible()
@@ -79,19 +79,19 @@ describe('WorkersList', () => {
await userEvent.click(screen.getByRole('button', { name: 'Next page' }))
expect(rowNames()).toEqual(['worker-10', 'worker-11'])
expect(rowNames()).toEqual(['instance-10', 'instance-11'])
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled()
})
it('returns to the first page when a search shrinks the results', async () => {
const workers = Array.from({ length: 12 }, (_, index) => worker(`worker-${index}`))
renderList(workers)
const instances = Array.from({ length: 12 }, (_, index) => instance(`instance-${index}`))
renderList(instances)
await userEvent.click(screen.getByRole('button', { name: 'Next page' }))
expect(screen.getByText('Page 2 of 2')).toBeVisible()
await userEvent.type(screen.getByPlaceholderText('Search by name'), 'worker-1')
await userEvent.type(screen.getByPlaceholderText('Search by name'), 'instance-1')
expect(screen.getByText('Page 1 of 1')).toBeVisible()
expect(rowNames()).toEqual(['worker-1', 'worker-10', 'worker-11'])
expect(rowNames()).toEqual(['instance-1', 'instance-10', 'instance-11'])
})
})
@@ -21,29 +21,33 @@ import {
TableRow,
} from 'ui'
import { COMPUTE_REGION_SHORT } from './Compute.constants'
import type {
ComputeInstance,
ComputeInstanceAccess,
ComputeInstanceBuildState,
} from './Compute.types'
import { filterComputeInstances, formatResources, getPage } from './Compute.utils'
import { ComputeInstanceStatePill } from './ComputeInstanceStatePill'
import { RuntimeBadge } from './RuntimeBadge'
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'
interface WorkersListProps {
interface ComputeListProps {
projectRef: string
workers: Worker[]
instances: ComputeInstance[]
onDeploy: () => void
onRefresh: () => void
isRefreshing: boolean
}
const STATE_FILTERS: { value: WorkerBuildState | 'all'; label: string }[] = [
const STATE_FILTERS: { value: ComputeInstanceBuildState | 'all'; label: string }[] = [
{ value: 'all', label: 'All states' },
{ value: 'active', label: 'Active' },
{ value: 'building', label: 'Building' },
{ value: 'failed', label: 'Failed' },
]
const ACCESS_FILTERS: { value: WorkerAccess | 'all'; label: string }[] = [
const ACCESS_FILTERS: { value: ComputeInstanceAccess | 'all'; label: string }[] = [
{ value: 'all', label: 'All access' },
{ value: 'public', label: 'Public' },
{ value: 'private', label: 'Private' },
@@ -51,26 +55,26 @@ const ACCESS_FILTERS: { value: WorkerAccess | 'all'; label: string }[] = [
const PAGE_SIZE = 10
const parseStateFilter = (value: string): WorkerBuildState | 'all' =>
const parseStateFilter = (value: string): ComputeInstanceBuildState | 'all' =>
STATE_FILTERS.find((option) => option.value === value)?.value ?? 'all'
const parseAccessFilter = (value: string): WorkerAccess | 'all' =>
const parseAccessFilter = (value: string): ComputeInstanceAccess | 'all' =>
ACCESS_FILTERS.find((option) => option.value === value)?.value ?? 'all'
export const WorkersList = ({
export const ComputeList = ({
projectRef,
workers,
instances,
onDeploy,
onRefresh,
isRefreshing,
}: WorkersListProps) => {
}: ComputeListProps) => {
const router = useRouter()
const [search, setSearch] = useState('')
const [stateFilter, setStateFilter] = useState<WorkerBuildState | 'all'>('all')
const [accessFilter, setAccessFilter] = useState<WorkerAccess | 'all'>('all')
const [stateFilter, setStateFilter] = useState<ComputeInstanceBuildState | 'all'>('all')
const [accessFilter, setAccessFilter] = useState<ComputeInstanceAccess | 'all'>('all')
const [page, setPage] = useState(1)
const filtered = filterWorkers(workers, {
const filtered = filterComputeInstances(instances, {
search,
state: stateFilter,
access: accessFilter,
@@ -79,7 +83,7 @@ export const WorkersList = ({
const resetToFirstPage = () => setPage(1)
const workerPagePath = (name: string) => `/project/${projectRef}/workers/${name}`
const computeInstancePagePath = (name: string) => `/project/${projectRef}/compute/${name}`
return (
<div className="space-y-4">
@@ -136,7 +140,7 @@ export const WorkersList = ({
Refresh
</Button>
<Button variant="primary" icon={<Terminal />} onClick={onDeploy}>
Deploy a worker
Deploy
</Button>
</div>
</div>
@@ -157,39 +161,42 @@ export const WorkersList = ({
{filtered.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center text-foreground-lighter">
No workers match your filters
No instances match your filters
</TableCell>
</TableRow>
)}
{paged.map((worker) => (
{paged.map((instance) => (
<TableRow
key={worker.name}
key={instance.name}
className="cursor-pointer"
onClick={() => router.push(workerPagePath(worker.name))}
onClick={() => router.push(computeInstancePagePath(instance.name))}
>
<TableCell className="font-medium text-foreground">
<Link href={workerPagePath(worker.name)} onClick={(e) => e.stopPropagation()}>
{worker.name}
<Link
href={computeInstancePagePath(instance.name)}
onClick={(e) => e.stopPropagation()}
>
{instance.name}
</Link>
</TableCell>
<TableCell>
<WorkerStatePill worker={worker} />
<ComputeInstanceStatePill instance={instance} />
</TableCell>
<TableCell>
<RuntimeBadge runtime={worker.runtime} />
<RuntimeBadge runtime={instance.runtime} />
</TableCell>
<TableCell>
{worker.access === 'public' ? (
{instance.access === 'public' ? (
<Badge variant="success">Public</Badge>
) : (
<Badge>Private</Badge>
)}
</TableCell>
<TableCell className="hidden text-foreground-light xl:table-cell">
{WORKERS_REGION_SHORT}
{COMPUTE_REGION_SHORT}
</TableCell>
<TableCell className="hidden text-foreground-light lg:table-cell">
{formatResources(worker)}
{formatResources(instance)}
</TableCell>
</TableRow>
))}
@@ -199,7 +206,7 @@ export const WorkersList = ({
{filtered.length > 0 && (
<CardFooter className="flex items-center justify-between border-t p-4">
<p className="text-sm text-foreground-muted">
Showing {startIndex + 1} to {startIndex + paged.length} of {filtered.length} worker
Showing {startIndex + 1} to {startIndex + paged.length} of {filtered.length} instance
{filtered.length === 1 ? '' : 's'}
</p>
<div className="flex items-center gap-x-2" aria-label="Pagination">
@@ -23,53 +23,60 @@ import { Admonition } from 'ui-patterns/Admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import {
COMPUTE_INSTANCE_DEPLOYABLE_RUNTIMES,
COMPUTE_INSTANCE_SIZES,
COMPUTE_REGION,
} from './Compute.constants'
import type { ComputeInstanceAccess } from './Compute.types'
import { formatSize, generateComputeInstanceName } from './Compute.utils'
import { ComputeInstanceSnippetTabs } from './ComputeInstanceSnippetTabs'
import { RuntimeBadge } from './RuntimeBadge'
import { WORKER_DEPLOYABLE_RUNTIMES, WORKER_SIZES, WORKERS_REGION } from './Workers.constants'
import type { WorkerAccess } from './Workers.types'
import { formatSize, generateWorkerName } from './Workers.utils'
import { WorkerSnippetTabs } from './WorkerSnippetTabs'
const FORM_ID = 'deploy-worker-form'
const FORM_ID = 'deploy-instance-form'
const DeployWorkerFormSchema = z.object({
const DeployComputeInstanceFormSchema = z.object({
name: z
.string()
.trim()
.min(1, 'Name is required')
.regex(/^[a-z0-9-]+$/, 'Lowercase letters, numbers, and hyphens only'),
runtime: z.enum(WORKER_DEPLOYABLE_RUNTIMES),
size: z.enum(WORKER_SIZES),
runtime: z.enum(COMPUTE_INSTANCE_DEPLOYABLE_RUNTIMES),
size: z.enum(COMPUTE_INSTANCE_SIZES),
access: z.enum(['private', 'public']),
instances: z
.union([z.literal(''), z.coerce.number().int().gte(1).lte(10)])
.refine((value) => value !== '', 'Instances is required'),
})
type DeployWorkerFormValues = z.infer<typeof DeployWorkerFormSchema>
type DeployComputeInstanceFormValues = z.infer<typeof DeployComputeInstanceFormSchema>
const DEFAULT_VALUES: DeployWorkerFormValues = {
const DEFAULT_VALUES: DeployComputeInstanceFormValues = {
name: '',
runtime: 'deno',
size: WORKER_SIZES[0],
size: COMPUTE_INSTANCE_SIZES[0],
access: 'private',
instances: 1,
}
const ACCESS_OPTIONS: { value: WorkerAccess; label: string }[] = [
const ACCESS_OPTIONS: { value: ComputeInstanceAccess; label: string }[] = [
{ value: 'private', label: 'Private' },
{ value: 'public', label: 'Public' },
]
interface DeployWorkerDialogProps {
interface DeployComputeInstanceDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogProps) => {
const form = useForm<DeployWorkerFormValues>({
export const DeployComputeInstanceDialog = ({
open,
onOpenChange,
}: DeployComputeInstanceDialogProps) => {
const form = useForm<DeployComputeInstanceFormValues>({
mode: 'onBlur',
resolver: zodResolver(DeployWorkerFormSchema),
defaultValues: { ...DEFAULT_VALUES, name: generateWorkerName() },
resolver: zodResolver(DeployComputeInstanceFormSchema),
defaultValues: { ...DEFAULT_VALUES, name: generateComputeInstanceName() },
})
const [name, runtime, size, access, instances] = useWatch({
@@ -81,7 +88,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent size="large">
<DialogHeader>
<DialogTitle>Deploy a worker</DialogTitle>
<DialogTitle>Deploy</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
@@ -89,7 +96,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
<Admonition
type="note"
title="This dashboard is read-only during the Private Alpha"
description={`Configure a worker below, then deploy it locally. Workers only deploy to ${WORKERS_REGION} during alpha.`}
description={`Configure an instance below, then deploy it locally. Compute instances only deploy to ${COMPUTE_REGION} during alpha.`}
className="border-x-0 border-y-0 rounded-none"
/>
@@ -104,7 +111,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
render={({ field }) => (
<FormItemLayout label="Name">
<FormControl>
<Input {...field} placeholder="my-worker" />
<Input {...field} placeholder="my-instance" />
</FormControl>
</FormItemLayout>
)}
@@ -122,7 +129,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
</SelectTrigger>
</FormControl>
<SelectContent>
{WORKER_DEPLOYABLE_RUNTIMES.map((value) => (
{COMPUTE_INSTANCE_DEPLOYABLE_RUNTIMES.map((value) => (
<SelectItem key={value} value={value}>
<RuntimeBadge runtime={value} />
</SelectItem>
@@ -148,7 +155,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
</SelectTrigger>
</FormControl>
<SelectContent>
{WORKER_SIZES.map((value) => (
{COMPUTE_INSTANCE_SIZES.map((value) => (
<SelectItem key={value} value={value}>
{formatSize(value)}
</SelectItem>
@@ -165,7 +172,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
render={({ field }) => (
<FormItemLayout
label="Access"
description="Public workers accept requests with a publishable key"
description="Public instances accept requests with a publishable key"
>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
@@ -213,7 +220,7 @@ export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogPro
<DialogSectionSeparator />
<DialogSection>
<WorkerSnippetTabs
<ComputeInstanceSnippetTabs
input={{
name: name ?? '',
runtime: runtime ?? DEFAULT_VALUES.runtime,
@@ -1,6 +1,6 @@
import { cn } from 'ui'
import { formatRuntime, getRuntimeMeta } from './Workers.utils'
import { formatRuntime, getRuntimeMeta } from './Compute.utils'
interface RuntimeBadgeProps {
runtime: string | undefined
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import {
buildComputeInstanceCliCommands,
buildComputeInstanceSnippets,
EXAMPLE_COMPUTE_INSTANCE,
} from './computeInstanceSnippets'
const input = (overrides: Partial<Parameters<typeof buildComputeInstanceSnippets>[0]> = {}) => ({
...EXAMPLE_COMPUTE_INSTANCE,
endpoint: 'abcdefgh.supabase.co',
...overrides,
})
describe('buildComputeInstanceSnippets', () => {
it('passes private exposure to the CLI and config', () => {
const { cli, configToml } = buildComputeInstanceSnippets(input({ access: 'private' }))
expect(cli).toContain('--exposure private')
expect(configToml).toContain('exposure = "private"')
})
})
describe('buildComputeInstanceCliCommands', () => {
it('targets the requested instance in every management command', () => {
const commands = buildComputeInstanceCliCommands('embed')
expect(commands).toHaveLength(4)
expect(commands.every(({ command }) => command.includes('embed'))).toBe(true)
})
})
@@ -1,19 +1,19 @@
import { RUNTIMES, WORKERS_REGION, workerUrl } from './Workers.constants'
import type { WorkerAccess } from './Workers.types'
import { formatSize } from './Workers.utils'
import { CLI_NAME } from '@/lib/constants/workers'
import { COMPUTE_REGION, computeInstanceUrl, RUNTIMES } from './Compute.constants'
import type { ComputeInstanceAccess } from './Compute.types'
import { formatSize } from './Compute.utils'
import { CLI_NAME } from '@/lib/constants/compute'
export interface WorkerSnippetInput {
export interface ComputeInstanceSnippetInput {
name: string
endpoint: string | undefined
protocol?: string
runtime: string | undefined
size: string
access: WorkerAccess
access: ComputeInstanceAccess
instances: number
}
export interface WorkerSnippets {
export interface ComputeInstanceSnippets {
aiPrompt: string
configToml: string
cli: string
@@ -22,22 +22,26 @@ export interface WorkerSnippets {
python: string
}
export const EXAMPLE_WORKER: Omit<WorkerSnippetInput, 'endpoint' | 'protocol'> = {
name: 'my-worker',
runtime: 'node',
size: '2gb-1vcpu',
access: 'public',
instances: 1,
}
export const EXAMPLE_COMPUTE_INSTANCE: Omit<ComputeInstanceSnippetInput, 'endpoint' | 'protocol'> =
{
name: 'my-instance',
runtime: 'node',
size: '2gb-1vcpu',
access: 'public',
instances: 1,
}
const safeName = (name: string) => (name.trim().length > 0 ? name.trim() : 'my-worker')
const safeName = (name: string) => (name.trim().length > 0 ? name.trim() : 'my-instance')
export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets {
export function buildComputeInstanceSnippets(
input: ComputeInstanceSnippetInput
): ComputeInstanceSnippets {
const name = safeName(input.name)
const runtime = input.runtime ?? 'node'
const url =
workerUrl({ endpoint: input.endpoint, protocol: input.protocol, name }) ?? '[YOUR WORKER URL]'
computeInstanceUrl({ endpoint: input.endpoint, protocol: input.protocol, name }) ??
'[YOUR INSTANCE URL]'
const cli = [
`supabase ${CLI_NAME} new ${name} --runtime ${runtime}`,
@@ -56,7 +60,7 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets {
`size = "${input.size}" # ${formatSize(input.size)}`,
`exposure = "${input.access}"`,
`instances = ${input.instances}`,
`# region is locked to ${WORKERS_REGION} at alpha`,
`# region is locked to ${COMPUTE_REGION} at alpha`,
].join('\n')
const configToml = [`# supabase/config.toml`, ``, configBlock].join('\n')
@@ -66,7 +70,7 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets {
const entrypointFile = runtimeMeta.entrypoint.split(' ').pop()
const aiPrompt = [
`Scaffold and deploy a Supabase Workers worker named "${name}" using the ${runtimeMeta.label} runtime:`,
`Scaffold and deploy a Supabase Compute instance named "${name}" using the ${runtimeMeta.label} runtime:`,
``,
`1. Create a supabase/${CLI_NAME}/${name}/ directory with a ${runtimeMeta.label} entrypoint (${entrypointFile}) that responds with "Hello, world!".`,
``,
@@ -106,17 +110,17 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets {
return { aiPrompt, configToml, cli, curl, javascript, python }
}
export interface WorkerCliCommand {
export interface ComputeInstanceCliCommand {
comment: string
command: string
}
export function buildWorkerCliCommands(name: string): WorkerCliCommand[] {
export function buildComputeInstanceCliCommands(name: string): ComputeInstanceCliCommand[] {
const slug = safeName(name)
return [
{ comment: 'Recreate the source locally', command: `supabase ${CLI_NAME} pull ${slug}` },
{ comment: 'Deploy a new version', command: `supabase ${CLI_NAME} push ${slug}` },
{ comment: 'Stream logs', command: `supabase ${CLI_NAME} logs ${slug} --follow` },
{ comment: 'Delete the worker', command: `supabase ${CLI_NAME} delete ${slug}` },
{ comment: 'Delete the instance', command: `supabase ${CLI_NAME} delete ${slug}` },
]
}
@@ -27,7 +27,7 @@ import { DOCS_URL } from '@/lib/constants'
export const EdgeFunctionSecrets = () => {
const { ref: projectRef } = useParams()
const workersEnabled = useFlag('workers')
const computeEnabled = useFlag('compute')
const [searchString, setSearchString] = useState('')
const { can: canReadSecrets, isLoading: isLoadingSecretsPermissions } = useAsyncCheckPermissions(
@@ -232,11 +232,11 @@ export const EdgeFunctionSecrets = () => {
}
}}
>
{workersEnabled ? (
{computeEnabled ? (
<p className="text-sm">
Ensure none of your <span className="font-medium">edge functions</span> or{' '}
<span className="font-medium">workers</span> are actively using this secret before
deleting it. This action cannot be undone.
<span className="font-medium">compute instances</span> are actively using this secret
before deleting it. This action cannot be undone.
</p>
) : (
<p className="text-sm">
@@ -4,10 +4,10 @@ import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import type { LogData } from '../Logs.types'
import { RowLayout, SeverityFormatter, TextFormatter } from '../LogsFormatters'
export const WorkersLogsColumnRender: Column<LogData>[] = [
export const ComputeLogsColumnRender: Column<LogData>[] = [
{
name: 'workers-logs-first-column',
key: 'workers-logs-first-column',
name: 'compute-logs-first-column',
key: 'compute-logs-first-column',
renderHeaderCell: () => null,
renderCell: (props) => (
<RowLayout>
@@ -278,7 +278,7 @@ const ProjectLinks = () => {
])
const authOverviewPageEnabled = useFlag('authOverviewPage')
const workersEnabled = useFlag('workers')
const computeEnabled = useFlag('compute')
const toolRoutes = useGenerateToolRoutes()
const productRoutes = generateProductRoutes(ref, project, {
@@ -287,7 +287,7 @@ const ProjectLinks = () => {
storage: storageEnabled,
realtime: realtimeEnabled,
authOverviewPage: authOverviewPageEnabled,
workers: workersEnabled,
compute: computeEnabled,
})
const otherRoutes = useGenerateOtherRoutes()
const settingsRoutes = generateSettingsRoutes(ref)
@@ -27,14 +27,14 @@ export const LOG_TYPES_LABELS = {
supavisor: 'Supavisor',
pgbouncer: 'PgBouncer',
multigres: 'Multigres',
workers: 'Workers',
compute: 'Compute',
}
type LogType = keyof typeof LOG_TYPES_LABELS
export const LOG_TYPES = Object.keys(LOG_TYPES_LABELS) as [LogType, ...LogType[]]
export const DEFAULT_LOG_TYPES = ['postgres', 'edge'] as const
export const LOG_TYPE_TO_SOURCE: Record<Exclude<LogType, 'workers'>, string> = {
export const LOG_TYPE_TO_SOURCE: Record<Exclude<LogType, 'compute'>, string> = {
edge: 'edge_logs',
postgrest: 'postgrest_logs',
storage: 'storage_logs',
@@ -11,10 +11,10 @@ describe('useFilterSearchSync', () => {
it('defers platform filter synchronization until feature flags load', () => {
vi.useFakeTimers()
const applyFilterSearch = vi.fn()
const workersFilter = [{ id: 'log_type', value: ['workers'] }]
const computeFilter = [{ id: 'log_type', value: ['compute'] }]
const { rerender } = renderHook(
({ enabled }) =>
useFilterSearchSync({ applyFilterSearch, columnFilters: workersFilter, enabled }),
useFilterSearchSync({ applyFilterSearch, columnFilters: computeFilter, enabled }),
{ initialProps: { enabled: false } }
)
@@ -46,32 +46,32 @@ describe('UnifiedLogs.queries (OTEL flat)', () => {
expect(where).not.toContain(`log_attributes['request.path'] LIKE '%/storage/%'`)
})
it('routes the `workers` log type to every worker OTEL stream', () => {
const sql = getUnifiedLogsQuery(withFilters('log_type:eq:workers'))
it('routes the `compute` log type to every worker OTEL stream', () => {
const sql = getUnifiedLogsQuery(withFilters('log_type:eq:compute'))
const where = sql.split(/\bWHERE\b/)[1] ?? ''
expect(where).toContain(
`log_attributes['source'] IN ('worker_ingress_logs','worker_guest_logs','worker_api_logs')`
)
expect(where).not.toContain(`source = 'workers'`)
expect(where).not.toContain(`source = 'compute'`)
})
it('classifies every worker OTEL stream as workers in the projected log type', () => {
it('classifies every worker OTEL stream as compute in the projected log type', () => {
const sql = getUnifiedLogsQuery(baseSearch)
expect(sql).toContain(
`WHEN log_attributes['source'] IN ('worker_ingress_logs','worker_guest_logs','worker_api_logs') THEN 'workers'`
`WHEN log_attributes['source'] IN ('worker_ingress_logs','worker_guest_logs','worker_api_logs') THEN 'compute'`
)
})
it('excludes every worker OTEL stream when the workers log type is negated', () => {
const sql = getUnifiedLogsQuery(withFilters('log_type:neq:workers'))
it('excludes every worker OTEL stream when the compute log type is negated', () => {
const sql = getUnifiedLogsQuery(withFilters('log_type:neq:compute'))
const where = sql.split(/\bWHERE\b/)[1] ?? ''
expect(where).toContain(
`NOT (log_attributes['source'] IN ('worker_ingress_logs','worker_guest_logs','worker_api_logs'))`
)
})
it('projects only Workers fields that exist on worker logs', () => {
const sql = getUnifiedLogsQuery(withFilters('log_type:eq:workers'))
it('projects only Compute fields that exist on worker logs', () => {
const sql = getUnifiedLogsQuery(withFilters('log_type:eq:compute'))
const workerCondition =
"log_attributes['source'] IN ('worker_ingress_logs','worker_guest_logs','worker_api_logs')"
@@ -298,8 +298,8 @@ describe('UnifiedLogs.queries (OTEL flat)', () => {
)
})
it('does not classify Workers rows into a severity bucket', () => {
const sql = getLogsChartQuery(withFilters('log_type:eq:workers'))
it('does not classify Compute rows into a severity bucket', () => {
const sql = getLogsChartQuery(withFilters('log_type:eq:compute'))
const workerCondition =
"log_attributes['source'] IN ('worker_ingress_logs','worker_guest_logs','worker_api_logs')"
@@ -13,7 +13,7 @@ import {
safeSql,
type SafeLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import { WORKER_LOG_SOURCES } from '@/lib/constants/workers'
import { WORKER_LOG_SOURCES } from '@/lib/constants/compute'
// Operator fragments for SQL emission. `safeSql` rejects plain strings, so we
// pre-brand the keywords we want to switch between.
@@ -68,7 +68,7 @@ const LOG_TYPE_CONDITION: Record<string, SafeLogSqlFragment> = {
safeSql`source = ${lit(source)}`,
])
),
workers: WORKER_LOG_SOURCE_CONDITION,
compute: WORKER_LOG_SOURCE_CONDITION,
}
// Derived `log_type` column for SELECT / GROUP BY / countIf use.
@@ -85,7 +85,7 @@ const LOG_TYPE_EXPR: SafeLogSqlFragment = safeSql`CASE
WHEN source = 'supavisor_logs' THEN 'supavisor'
WHEN source = 'pgbouncer_logs' THEN 'pgbouncer'
WHEN source = 'multigres_logs' THEN 'multigres'
WHEN ${WORKER_LOG_SOURCE_CONDITION} THEN 'workers'
WHEN ${WORKER_LOG_SOURCE_CONDITION} THEN 'compute'
ELSE source
END`
@@ -49,9 +49,9 @@ import { QuerySearchParamsType } from './UnifiedLogs.types'
import {
gateLogTypeFilters,
gateLogTypeOptions,
getComputeLogsAvailability,
getFacetedUniqueValues,
getLevelRowClassName,
getWorkersLogsAvailability,
} from './UnifiedLogs.utils'
import { LEVELS } from '@/components/ui/DataTable/DataTable.constants'
import { Option } from '@/components/ui/DataTable/DataTable.types'
@@ -97,15 +97,15 @@ export const UnifiedLogs = () => {
const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
const showMultigresLogs = useShowMultigresLogs()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const workersEnabled = !!useFlag('workers')
const workersAvailability = getWorkersLogsAvailability({
const computeEnabled = !!useFlag('compute')
const computeAvailability = getComputeLogsAvailability({
isPlatform: IS_PLATFORM,
flagsLoaded,
workersEnabled,
computeEnabled,
})
const visibleSearchFilters = gateLogTypeFilters(search.filter, {
multigres: showMultigresLogs,
workers: workersAvailability.preserveWorkersFilter,
compute: computeAvailability.preserveComputeFilter,
})
const defaultColumnSorting = search.sort ? [search.sort] : []
@@ -165,11 +165,11 @@ export const UnifiedLogs = () => {
parameters.filter =
gateLogTypeFilters(parameters.filter, {
multigres: showMultigresLogs,
workers: workersAvailability.canQueryWorkers,
compute: computeAvailability.canQueryCompute,
}) ?? null
}
return parameters
}, [search, showMultigresLogs, workersAvailability.canQueryWorkers])
}, [search, showMultigresLogs, computeAvailability.canQueryCompute])
const {
data: unifiedLogsData,
@@ -298,7 +298,7 @@ export const UnifiedLogs = () => {
const filterFields = useMemo(() => {
const gatedFields = gateLogTypeOptions(defaultFilterFields, {
multigres: showMultigresLogs,
workers: workersAvailability.canQueryWorkers,
compute: computeAvailability.canQueryCompute,
})
return gatedFields.map((field) => {
@@ -327,14 +327,14 @@ export const UnifiedLogs = () => {
return { ...field, options }
})
}, [facets, showMultigresLogs, workersAvailability.canQueryWorkers])
}, [facets, showMultigresLogs, computeAvailability.canQueryCompute])
const applyFilterSearch = () => {
const update = buildFilterSearchUpdate(columnFilters, filterFields)
if (Array.isArray(update.filter)) {
update.filter = gateLogTypeFilters(update.filter.map(String), {
multigres: showMultigresLogs,
workers: workersAvailability.canQueryWorkers,
compute: computeAvailability.canQueryCompute,
})
}
setSearch(update)
@@ -343,7 +343,7 @@ export const UnifiedLogs = () => {
useFilterSearchSync({
applyFilterSearch,
columnFilters,
enabled: workersAvailability.readyToSyncFilters,
enabled: computeAvailability.readyToSyncFilters,
})
useEffect(() => {
@@ -4,9 +4,9 @@ import {
buildUnifiedLogsUrl,
gateLogTypeFilters,
gateLogTypeOptions,
getComputeLogsAvailability,
getEventMessageDisplay,
getRawLogData,
getWorkersLogsAvailability,
parseMultigresEventMessage,
} from './UnifiedLogs.utils'
@@ -98,7 +98,7 @@ describe('getEventMessageDisplay', () => {
})
describe('getRawLogData', () => {
it('returns only the real Workers payload fields', () => {
it('returns only the real Compute payload fields', () => {
const row = {
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
@@ -112,7 +112,7 @@ describe('getRawLogData', () => {
},
project: 'cxkpapyhaaywrtudnqpl',
timestamp: 1788424716876000,
log_type: 'workers' as const,
log_type: 'compute' as const,
status: null,
level: null,
method: null,
@@ -129,7 +129,7 @@ describe('getRawLogData', () => {
})
})
it('returns non-Workers rows unchanged', () => {
it('returns non-Compute rows unchanged', () => {
const row = {
id: 'edge-log',
timestamp: 1788424716876000,
@@ -153,30 +153,30 @@ describe('gateLogTypeOptions', () => {
options: [
{ label: 'Postgres', value: 'postgres' },
{ label: 'Multigres', value: 'multigres' },
{ label: 'Workers', value: 'workers' },
{ label: 'Compute', value: 'compute' },
],
},
]
it('drops log_type options whose flags are disabled', () => {
const gated = gateLogTypeOptions(fields, { multigres: false, workers: false })
const gated = gateLogTypeOptions(fields, { multigres: false, compute: false })
const logType = gated.find((field) => field.value === 'log_type')
expect(logType?.options?.map((option) => option.value)).toEqual(['postgres'])
})
it('keeps independently enabled log types', () => {
const gated = gateLogTypeOptions(fields, { multigres: false, workers: true })
const gated = gateLogTypeOptions(fields, { multigres: false, compute: true })
const logType = gated.find((field) => field.value === 'log_type')
expect(logType?.options?.map((option) => option.value)).toEqual(['postgres', 'workers'])
expect(logType?.options?.map((option) => option.value)).toEqual(['postgres', 'compute'])
})
it('returns the original fields when every gated log type is enabled', () => {
const gated = gateLogTypeOptions(fields, { multigres: true, workers: true })
const gated = gateLogTypeOptions(fields, { multigres: true, compute: true })
expect(gated).toBe(fields)
})
it('leaves non log_type fields untouched', () => {
const gated = gateLogTypeOptions(fields, { workers: false })
const gated = gateLogTypeOptions(fields, { compute: false })
expect(gated.find((field) => field.value === 'date')).toEqual({ value: 'date' })
})
})
@@ -185,77 +185,77 @@ describe('gateLogTypeFilters', () => {
it('removes disabled log types from equality and inequality filters', () => {
expect(
gateLogTypeFilters(
['log_type:eq:workers', 'log_type:neq:multigres', 'log_type:eq:postgres', 'method:eq:GET'],
{ workers: false, multigres: false }
['log_type:eq:compute', 'log_type:neq:multigres', 'log_type:eq:postgres', 'method:eq:GET'],
{ compute: false, multigres: false }
)
).toEqual(['log_type:eq:postgres', 'method:eq:GET'])
})
it('keeps enabled log types and unrelated filters unchanged', () => {
const filters = ['log_type:eq:workers', 'method:eq:GET']
expect(gateLogTypeFilters(filters, { workers: true })).toBe(filters)
const filters = ['log_type:eq:compute', 'method:eq:GET']
expect(gateLogTypeFilters(filters, { compute: true })).toBe(filters)
})
it('preserves absent filter values', () => {
expect(gateLogTypeFilters(undefined, { workers: false })).toBeUndefined()
expect(gateLogTypeFilters(null, { workers: false })).toBeNull()
expect(gateLogTypeFilters(undefined, { compute: false })).toBeUndefined()
expect(gateLogTypeFilters(null, { compute: false })).toBeNull()
})
})
describe('getWorkersLogsAvailability', () => {
const workersFilter = ['log_type:eq:workers']
describe('getComputeLogsAvailability', () => {
const computeFilter = ['log_type:eq:compute']
it('preserves an unresolved platform filter without allowing it into queries or sync', () => {
const availability = getWorkersLogsAvailability({
const availability = getComputeLogsAvailability({
isPlatform: true,
flagsLoaded: false,
workersEnabled: false,
computeEnabled: false,
})
expect(gateLogTypeFilters(workersFilter, { workers: availability.preserveWorkersFilter })).toBe(
workersFilter
expect(gateLogTypeFilters(computeFilter, { compute: availability.preserveComputeFilter })).toBe(
computeFilter
)
expect(gateLogTypeFilters(workersFilter, { workers: availability.canQueryWorkers })).toEqual([])
expect(gateLogTypeFilters(computeFilter, { compute: availability.canQueryCompute })).toEqual([])
expect(availability.readyToSyncFilters).toBe(false)
})
it('allows Workers filters and queries when the platform flag is enabled', () => {
const availability = getWorkersLogsAvailability({
it('allows Compute filters and queries when the platform flag is enabled', () => {
const availability = getComputeLogsAvailability({
isPlatform: true,
flagsLoaded: true,
workersEnabled: true,
computeEnabled: true,
})
expect(availability).toEqual({
canQueryWorkers: true,
preserveWorkersFilter: true,
canQueryCompute: true,
preserveComputeFilter: true,
readyToSyncFilters: true,
})
})
it('removes Workers filters and queries when the platform flag is disabled', () => {
const availability = getWorkersLogsAvailability({
it('removes Compute filters and queries when the platform flag is disabled', () => {
const availability = getComputeLogsAvailability({
isPlatform: true,
flagsLoaded: true,
workersEnabled: false,
computeEnabled: false,
})
expect(availability).toEqual({
canQueryWorkers: false,
preserveWorkersFilter: false,
canQueryCompute: false,
preserveComputeFilter: false,
readyToSyncFilters: true,
})
})
it('syncs generic filters immediately while keeping Workers unavailable on self-hosted', () => {
const availability = getWorkersLogsAvailability({
it('syncs generic filters immediately while keeping Compute unavailable on self-hosted', () => {
const availability = getComputeLogsAvailability({
isPlatform: false,
workersEnabled: false,
computeEnabled: false,
})
expect(availability).toEqual({
canQueryWorkers: false,
preserveWorkersFilter: false,
canQueryCompute: false,
preserveComputeFilter: false,
readyToSyncFilters: true,
})
})
@@ -10,20 +10,20 @@ import type { UnifiedLogInspectionEntry } from '@/data/logs/unified-log-inspecti
export type UnifiedLogType = keyof typeof LOG_TYPES_LABELS
export function getWorkersLogsAvailability({
export function getComputeLogsAvailability({
isPlatform,
flagsLoaded,
workersEnabled,
computeEnabled,
}: {
isPlatform: boolean
flagsLoaded?: boolean
workersEnabled: boolean
computeEnabled: boolean
}) {
const flagsReady = flagsLoaded === true
return {
canQueryWorkers: isPlatform && flagsReady && workersEnabled,
preserveWorkersFilter: isPlatform && (!flagsReady || workersEnabled),
canQueryCompute: isPlatform && flagsReady && computeEnabled,
preserveComputeFilter: isPlatform && (!flagsReady || computeEnabled),
readyToSyncFilters: !isPlatform || flagsReady,
}
}
@@ -83,12 +83,12 @@ export function getRowTimestampMs(
return null
}
type WorkersRawLogData = Pick<ColumnSchema, 'id' | 'timestamp' | 'event_message' | 'metadata'>
type ComputeRawLogData = Pick<ColumnSchema, 'id' | 'timestamp' | 'event_message' | 'metadata'>
export function getRawLogData(
row: ColumnSchema | UnifiedLogInspectionEntry
): ColumnSchema | UnifiedLogInspectionEntry | WorkersRawLogData {
if (!('log_type' in row) || row.log_type !== 'workers') return row
): ColumnSchema | UnifiedLogInspectionEntry | ComputeRawLogData {
if (!('log_type' in row) || row.log_type !== 'compute') return row
return {
id: row.id,
@@ -1,4 +1,4 @@
import { Auth, EdgeFunctions, Realtime, Storage, Workers } from 'icons'
import { Auth, Compute, EdgeFunctions, Realtime, Storage } from 'icons'
import { Box, Cable, Code2, Database, Network } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
@@ -25,7 +25,7 @@ const ICON_MAP: Partial<Record<(typeof LOG_TYPES)[number], IconComponent>> = {
supavisor: Cable,
pgbouncer: Cable,
multigres: Network,
workers: Workers,
compute: Compute,
}
export const LogTypeIcon = ({
@@ -1,24 +0,0 @@
import type { components } from 'api-types'
type WorkerAttributes = components['schemas']['V2WorkerResponse_Output']['data']['attributes']
export type WorkerBuildState = WorkerAttributes['build_state']
export type WorkerInstanceTally = NonNullable<WorkerAttributes['instances']>
// The API types `spec.exposure` as a free-form string; the UI only handles these two values.
export type WorkerAccess = 'public' | 'private'
export interface Worker {
name: string
buildState: WorkerBuildState
isDeleting: boolean
runtime?: string
size: string
access: WorkerAccess
declaredInstances: number
instances?: WorkerInstanceTally
imageVersion?: string
stateReason?: string
instancesError?: string
}
@@ -1,27 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildWorkerCliCommands, buildWorkerSnippets, EXAMPLE_WORKER } from './workerSnippets'
const input = (overrides: Partial<Parameters<typeof buildWorkerSnippets>[0]> = {}) => ({
...EXAMPLE_WORKER,
endpoint: 'abcdefgh.supabase.co',
...overrides,
})
describe('buildWorkerSnippets', () => {
it('passes private exposure to the CLI and config', () => {
const { cli, configToml } = buildWorkerSnippets(input({ access: 'private' }))
expect(cli).toContain('--exposure private')
expect(configToml).toContain('exposure = "private"')
})
})
describe('buildWorkerCliCommands', () => {
it('targets the requested worker in every management command', () => {
const commands = buildWorkerCliCommands('embed')
expect(commands).toHaveLength(4)
expect(commands.every(({ command }) => command.includes('embed'))).toBe(true)
})
})
@@ -3,18 +3,18 @@ import type { CommandOptions } from 'ui-patterns/CommandMenu'
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import { PRODUCT_NAME } from '@/lib/constants/compute'
export function useWorkersGotoCommands(options?: CommandOptions) {
export function useComputeGotoCommands(options?: CommandOptions) {
let { ref } = useParams()
ref ||= '_'
useRegisterCommands(
COMMAND_MENU_SECTIONS.NAVIGATE,
[
{
id: 'nav-workers',
id: 'nav-compute',
name: PRODUCT_NAME,
route: `/project/${ref}/workers`,
route: `/project/${ref}/compute`,
defaultHidden: true,
},
],
@@ -10,9 +10,9 @@ import { ProductMenu } from '@/components/ui/ProductMenu'
import type { ProductMenuGroup } from '@/components/ui/ProductMenu/ProductMenu.types'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { withAuth } from '@/hooks/misc/withAuth'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import { PRODUCT_NAME } from '@/lib/constants/compute'
const useGenerateWorkersMenu = (): ProductMenuGroup[] => {
const useGenerateComputeMenu = (): ProductMenuGroup[] => {
const { ref: projectRef = 'default' } = useParams()
return useMemo(
() => [
@@ -23,13 +23,13 @@ const useGenerateWorkersMenu = (): ProductMenuGroup[] => {
name: PRODUCT_NAME,
key: 'main',
pages: ['', '[name]'],
url: `/project/${projectRef}/workers`,
url: `/project/${projectRef}/compute`,
items: [],
},
{
name: 'Secrets',
key: 'secrets',
url: `/project/${projectRef}/workers/secrets`,
url: `/project/${projectRef}/compute/secrets`,
items: [],
},
],
@@ -39,36 +39,36 @@ const useGenerateWorkersMenu = (): ProductMenuGroup[] => {
)
}
export const WorkersProductMenu = () => {
export const ComputeProductMenu = () => {
const router = useRouter()
const page = router.pathname.split('/')[4]
const menu = useGenerateWorkersMenu()
const menu = useGenerateComputeMenu()
return <ProductMenu page={page} menu={menu} />
}
interface WorkersLayoutProps {
interface ComputeLayoutProps {
title?: string
}
const WorkersLayoutContent = ({ children, title }: PropsWithChildren<WorkersLayoutProps>) => {
const ComputeLayoutContent = ({ children, title }: PropsWithChildren<ComputeLayoutProps>) => {
const router = useRouter()
const { ref: projectRef } = useParams()
const { hasLoaded } = useContext(FeatureFlagContext)
const workersEnabled = useFlag('workers')
// The v2 workers routes require the FGA workers_read permission, which shared-types does not
const computeEnabled = useFlag('compute')
// The v2 compute routes require the FGA workers_read permission, which shared-types does not
// expose yet; they reuse the Edge Functions OAuth scope, so gate on the same product here.
const { isLoading: isLoadingPermissions, can: canReadWorkers } = useAsyncCheckPermissions(
const { isLoading: isLoadingPermissions, can: canReadCompute } = useAsyncCheckPermissions(
PermissionAction.FUNCTIONS_READ,
'*'
)
useEffect(() => {
if (hasLoaded && !workersEnabled) {
if (hasLoaded && !computeEnabled) {
router.replace(`/project/${projectRef}`)
}
}, [router, hasLoaded, workersEnabled, projectRef])
}, [router, hasLoaded, computeEnabled, projectRef])
if (!workersEnabled) return null
if (!computeEnabled) return null
if (isLoadingPermissions) {
return (
@@ -84,17 +84,17 @@ const WorkersLayoutContent = ({ children, title }: PropsWithChildren<WorkersLayo
<ProjectLayout
product={PRODUCT_NAME}
productMenuBadge={<Badge variant="warning">Private Alpha</Badge>}
productMenu={<WorkersProductMenu />}
productMenu={<ComputeProductMenu />}
isBlocking={false}
browserTitle={{ entity: PRODUCT_NAME, section: title }}
>
{canReadWorkers ? (
{canReadCompute ? (
children
) : (
<NoPermission isFullPage resourceText="view this project's workers" />
<NoPermission isFullPage resourceText="view this project's compute instances" />
)}
</ProjectLayout>
)
}
export const WorkersLayout = withAuth(WorkersLayoutContent)
export const ComputeLayout = withAuth(ComputeLayoutContent)
@@ -85,9 +85,9 @@ describe('generateProductRoutes', () => {
expect(keys(routes)).toEqual(['database', 'auth', 'storage', 'functions', 'realtime'])
})
it('includes workers only when the workers flag is enabled', () => {
expect(keys(generateProductRoutes(REF, activeProject))).not.toContain('workers')
expect(keys(generateProductRoutes(REF, activeProject, { workers: true }))).toContain('workers')
it('includes compute only when the compute flag is enabled', () => {
expect(keys(generateProductRoutes(REF, activeProject))).not.toContain('compute')
expect(keys(generateProductRoutes(REF, activeProject, { compute: true }))).toContain('compute')
})
it('excludes auth when auth feature is disabled', () => {
@@ -1,13 +1,13 @@
import { useParams } from 'common'
import {
Auth,
Compute,
Database,
EdgeFunctions,
Realtime,
SqlEditor,
Storage,
TableEditor,
Workers,
} from 'icons'
import { Blocks, Lightbulb, List, Settings, Telescope } from 'lucide-react'
@@ -22,7 +22,7 @@ import type { Project } from '@/data/projects/project-detail-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import { PRODUCT_NAME } from '@/lib/constants/compute'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
interface RouteContext {
@@ -38,7 +38,7 @@ interface ProductFeatures {
storage?: boolean
realtime?: boolean
authOverviewPage?: boolean
workers?: boolean
compute?: boolean
}
interface OtherFeatures {
@@ -110,7 +110,7 @@ export const generateProductRoutes = (
const storageEnabled = features?.storage ?? true
const realtimeEnabled = features?.realtime ?? true
const authOverviewPageEnabled = features?.authOverviewPage ?? false
const workersEnabled = features?.workers ?? false
const computeEnabled = features?.compute ?? false
return [
{
@@ -169,14 +169,14 @@ export const generateProductRoutes = (
},
]
: []),
...(workersEnabled
...(computeEnabled
? [
{
key: 'workers',
key: 'compute',
label: PRODUCT_NAME,
disabled: !isProjectActive,
icon: <Workers size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/workers`),
icon: <Compute size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/compute`),
isNew: true,
},
]
@@ -65,7 +65,7 @@ export function MobileMenuContent({
'realtime:all',
])
const authOverviewPageEnabled = useFlag('authOverviewPage')
const workersEnabled = useFlag('workers')
const computeEnabled = useFlag('compute')
const toolRoutes = useGenerateToolRoutes()
const productRoutes = useMemo(
@@ -76,7 +76,7 @@ export function MobileMenuContent({
storage: storageEnabled,
realtime: realtimeEnabled,
authOverviewPage: authOverviewPageEnabled,
workers: workersEnabled,
compute: computeEnabled,
}),
[
ref,
@@ -86,7 +86,7 @@ export function MobileMenuContent({
storageEnabled,
realtimeEnabled,
authOverviewPageEnabled,
workersEnabled,
computeEnabled,
]
)
const otherRoutes = useGenerateOtherRoutes()
@@ -4,6 +4,7 @@ import { useStorageGotoCommands } from '../interfaces/Storage/Storage.Commands'
import { useAdvisorsGoToCommands } from './AdvisorsLayout/Advisors.Commands'
import { useAuthGotoCommands } from './AuthLayout/Auth.Commands'
import { useBillingGotoCommands } from './BillingLayout/Billing.Commands'
import { useComputeGotoCommands } from './ComputeLayout/Compute.Commands'
import { useDatabaseGotoCommands } from './DatabaseLayout/Database.Commands'
import { useFunctionsGotoCommands } from './EdgeFunctionsLayout/EdgeFunctions.Commands'
import { useIntegrationsGotoCommands } from './IntegrationsLayout/Integrations.Commands'
@@ -12,12 +13,11 @@ import { useProjectSettingsGotoCommands } from './ProjectSettingsLayout/ProjectS
import { useReportsGotoCommands } from './ReportsLayout/Reports.Commands'
import { useSqlEditorGotoCommands } from './SQLEditorLayout/SqlEditor.Commands'
import { useTableEditorGotoCommands } from './TableEditorLayout/TableEditor.Commands'
import { useWorkersGotoCommands } from './WorkersLayout/Workers.Commands'
import { useApiDocsGotoCommands } from '@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands'
export function useLayoutNavCommands() {
const isLoggedIn = useIsLoggedIn()
const workersEnabled = useFlag('workers')
const computeEnabled = useFlag('compute')
useTableEditorGotoCommands({ enabled: isLoggedIn })
useSqlEditorGotoCommands({ enabled: isLoggedIn })
@@ -26,7 +26,7 @@ export function useLayoutNavCommands() {
useAdvisorsGoToCommands({ enabled: isLoggedIn })
useStorageGotoCommands({ enabled: isLoggedIn })
useFunctionsGotoCommands({ enabled: isLoggedIn })
useWorkersGotoCommands({ enabled: isLoggedIn && workersEnabled })
useComputeGotoCommands({ enabled: isLoggedIn && computeEnabled })
useLogsGotoCommands({ enabled: isLoggedIn })
useReportsGotoCommands({ enabled: isLoggedIn })
useApiDocsGotoCommands({ enabled: isLoggedIn })
@@ -1,43 +1,43 @@
import { describe, expect, it } from 'vitest'
import { workersKeys } from './keys'
import { parseWorkerLogRows, workerLogsSql } from './worker-logs-query'
import { computeInstanceLogsSql, parseComputeInstanceLogRows } from './compute-instance-logs-query'
import { computeKeys } from './keys'
describe('workerLogsSql', () => {
it('reads one worker stream, newest first', () => {
expect(workerLogsSql('embed', 'output')).toBe(
describe('computeInstanceLogsSql', () => {
it('reads one instance stream, newest first', () => {
expect(computeInstanceLogsSql('embed', 'output')).toBe(
"select id, timestamp, severity_text as severity, event_message as message from logs where log_attributes['worker'] = 'embed' and log_attributes['source'] = 'worker_guest_logs' order by timestamp desc limit 100"
)
})
it('filters by event message before applying the limit', () => {
expect(workerLogsSql('embed', 'requests', { message: 'timeout' })).toBe(
expect(computeInstanceLogsSql('embed', 'requests', { message: 'timeout' })).toBe(
"select id, timestamp, severity_text as severity, event_message as message from logs where log_attributes['worker'] = 'embed' and log_attributes['source'] = 'worker_ingress_logs' and event_message ilike '%timeout%' order by timestamp desc limit 100"
)
})
it('names the right stream for each tab', () => {
expect(workerLogsSql('embed', 'requests')).toContain("'worker_ingress_logs'")
expect(workerLogsSql('embed', 'builds')).toContain("'worker_api_logs'")
expect(computeInstanceLogsSql('embed', 'requests')).toContain("'worker_ingress_logs'")
expect(computeInstanceLogsSql('embed', 'builds')).toContain("'worker_api_logs'")
})
it('escapes a worker name rather than interpolating it raw', () => {
expect(workerLogsSql("embed' or '1'='1", 'output')).toContain(
it('escapes an instance name rather than interpolating it raw', () => {
expect(computeInstanceLogsSql("embed' or '1'='1", 'output')).toContain(
"log_attributes['worker'] = 'embed'' or ''1''=''1'"
)
})
it('escapes filter values rather than interpolating them raw', () => {
expect(workerLogsSql('embed', 'requests', { message: "can't connect" })).toContain(
expect(computeInstanceLogsSql('embed', 'requests', { message: "can't connect" })).toContain(
"event_message ilike '%can''t connect%'"
)
})
})
describe('workersKeys.logs', () => {
describe('computeKeys.logs', () => {
it('includes the selected time range and filters', () => {
expect(
workersKeys.logs('project-ref', 'embed', 'requests', {
computeKeys.logs('project-ref', 'embed', 'requests', {
iso_timestamp_start: '2026-09-01T12:00:00.000Z',
iso_timestamp_end: '2026-09-02T12:00:00.000Z',
message: 'timeout',
@@ -45,7 +45,7 @@ describe('workersKeys.logs', () => {
).toEqual([
'projects',
'project-ref',
'worker',
'instance',
'embed',
'logs',
'requests',
@@ -58,9 +58,9 @@ describe('workersKeys.logs', () => {
})
})
describe('parseWorkerLogRows', () => {
describe('parseComputeInstanceLogRows', () => {
it('shapes rows for the logs table with a numeric timestamp', () => {
const [row] = parseWorkerLogRows([
const [row] = parseComputeInstanceLogRows([
{ id: 'a', timestamp: '2026-08-24T10:00:00.000000', severity: 'ERROR', message: 'boom' },
])
expect(row.id).toBe('a')
@@ -71,7 +71,7 @@ describe('parseWorkerLogRows', () => {
})
it('falls back to empty strings for null severity and message', () => {
const [row] = parseWorkerLogRows([
const [row] = parseComputeInstanceLogRows([
{ id: 'a', timestamp: '2026-08-24T10:00:00.000000', severity: null, message: null },
])
expect(row.severity_text).toBe('')
@@ -79,10 +79,12 @@ describe('parseWorkerLogRows', () => {
})
it('returns an empty array for a missing result', () => {
expect(parseWorkerLogRows(undefined)).toEqual([])
expect(parseComputeInstanceLogRows(undefined)).toEqual([])
})
it('rejects a malformed row rather than passing bad data to the table', () => {
expect(() => parseWorkerLogRows([{ timestamp: '2026-08-24T10:00:00.000000' }])).toThrow()
expect(() =>
parseComputeInstanceLogRows([{ timestamp: '2026-08-24T10:00:00.000000' }])
).toThrow()
})
})
@@ -1,50 +1,51 @@
import { queryOptions } from '@tanstack/react-query'
import { z } from 'zod'
import { workersKeys } from './keys'
import { computeKeys } from './keys'
import type { LogData } from '@/components/interfaces/Settings/Logs/Logs.types'
import { otelTimestampToMicros } from '@/components/interfaces/Settings/Logs/Logs.utils.otel'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint'
import { analyticsLiteral, safeSql } from '@/data/logs/safe-analytics-sql'
import { IS_PLATFORM } from '@/lib/constants'
import { WORKER_LOG_SOURCES } from '@/lib/constants/workers'
import { WORKER_LOG_SOURCES } from '@/lib/constants/compute'
export type WorkerLogStream = keyof typeof WORKER_LOG_SOURCES
export type ComputeInstanceLogStream = keyof typeof WORKER_LOG_SOURCES
export const WORKER_LOG_STREAM_LABEL: Record<WorkerLogStream, string> = {
export const COMPUTE_INSTANCE_LOG_STREAM_LABEL: Record<ComputeInstanceLogStream, string> = {
requests: 'Invocations',
output: 'Logs',
builds: 'Activity',
}
// Both are read from `log_attributes` rather than the endpoint's own `source` column:
// that column is derived from a mapping which does not currently classify worker rows.
// that column is derived from a mapping which does not currently classify these rows.
// The key itself is still the pre-rename "worker" attribute the backend emits.
const WORKER_NAME_KEY = 'worker'
const STREAM_KEY = 'source'
const LOG_LIMIT = 100
const workerLogRowSchema = z.object({
const computeInstanceLogRowSchema = z.object({
id: z.string(),
timestamp: z.union([z.string(), z.number()]),
severity: z.string().nullish(),
message: z.string().nullish(),
})
export type WorkerLogsVariables = {
export type ComputeInstanceLogsVariables = {
projectRef?: string
name?: string
stream: WorkerLogStream
stream: ComputeInstanceLogStream
iso_timestamp_start: string
iso_timestamp_end: string
message?: string
}
export const workerLogsSql = (
export const computeInstanceLogsSql = (
name: string,
stream: WorkerLogStream,
{ message }: Pick<WorkerLogsVariables, 'message'> = {}
stream: ComputeInstanceLogStream,
{ message }: Pick<ComputeInstanceLogsVariables, 'message'> = {}
) => {
const messageFilter = message
? safeSql` and event_message ilike ${analyticsLiteral(`%${message}%`)}`
@@ -53,9 +54,9 @@ export const workerLogsSql = (
return safeSql`select id, timestamp, severity_text as severity, event_message as message from logs where log_attributes[${analyticsLiteral(WORKER_NAME_KEY)}] = ${analyticsLiteral(name)} and log_attributes[${analyticsLiteral(STREAM_KEY)}] = ${analyticsLiteral(WORKER_LOG_SOURCES[stream])}${messageFilter} order by timestamp desc limit ${analyticsLiteral(LOG_LIMIT)}`
}
export const parseWorkerLogRows = (result: unknown): LogData[] =>
export const parseComputeInstanceLogRows = (result: unknown): LogData[] =>
z
.array(workerLogRowSchema)
.array(computeInstanceLogRowSchema)
.parse(result ?? [])
.map((row) => ({
id: row.id,
@@ -64,7 +65,7 @@ export const parseWorkerLogRows = (result: unknown): LogData[] =>
severity_text: row.severity ?? '',
}))
async function getWorkerLogs(
async function getComputeInstanceLogs(
{
projectRef,
name,
@@ -72,7 +73,7 @@ async function getWorkerLogs(
iso_timestamp_start,
iso_timestamp_end,
message,
}: WorkerLogsVariables,
}: ComputeInstanceLogsVariables,
signal?: AbortSignal
): Promise<LogData[]> {
if (!projectRef) throw new Error('projectRef is required')
@@ -81,27 +82,27 @@ async function getWorkerLogs(
const data = await executeAnalyticsSql({
projectRef,
endpoint: logsAllEndpointUrl(true),
sql: workerLogsSql(name, stream, { message }),
sql: computeInstanceLogsSql(name, stream, { message }),
iso_timestamp_start,
iso_timestamp_end,
signal,
})
return parseWorkerLogRows(data?.result)
return parseComputeInstanceLogRows(data?.result)
}
export const workerLogsQueryOptions = (variables: WorkerLogsVariables) => {
export const computeInstanceLogsQueryOptions = (variables: ComputeInstanceLogsVariables) => {
const { projectRef, name, stream, iso_timestamp_start, iso_timestamp_end } = variables
const message = variables.message?.trim() || undefined
return queryOptions({
queryKey: workersKeys.logs(projectRef, name, stream, {
queryKey: computeKeys.logs(projectRef, name, stream, {
iso_timestamp_start,
iso_timestamp_end,
message,
}),
queryFn: ({ signal }) =>
getWorkerLogs(
getComputeInstanceLogs(
{ projectRef, name, stream, iso_timestamp_start, iso_timestamp_end, message },
signal
),
@@ -0,0 +1,35 @@
import { queryOptions } from '@tanstack/react-query'
import { parseComputeInstance } from './compute.utils'
import { computeKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
import { IS_PLATFORM } from '@/lib/constants'
import type { ResponseError } from '@/types'
export type ComputeInstanceVariables = { projectRef?: string; name?: string }
export type ComputeInstanceError = ResponseError
async function getComputeInstance(
{ projectRef, name }: ComputeInstanceVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
if (!name) throw new Error('name is required')
const { data, error } = await get('/v2/projects/{ref}/workers/{name}', {
params: { path: { ref: projectRef, name } },
signal,
})
if (error) return handleError(error)
return parseComputeInstance(data.data)
}
export type ComputeInstanceData = Awaited<ReturnType<typeof getComputeInstance>>
export const computeInstanceQueryOptions = ({ projectRef, name }: ComputeInstanceVariables) =>
queryOptions({
queryKey: computeKeys.detail(projectRef, name),
queryFn: ({ signal }) => getComputeInstance({ projectRef, name }, signal),
enabled: IS_PLATFORM && typeof projectRef !== 'undefined' && typeof name !== 'undefined',
})
+32
View File
@@ -0,0 +1,32 @@
import { queryOptions } from '@tanstack/react-query'
import { parseComputeInstance } from './compute.utils'
import { computeKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
import { IS_PLATFORM } from '@/lib/constants'
import type { ResponseError } from '@/types'
export type ComputeVariables = { projectRef?: string }
export type ComputeError = ResponseError
async function getComputeInstances({ projectRef }: ComputeVariables, signal?: AbortSignal) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await get('/v2/projects/{ref}/workers', {
params: { path: { ref: projectRef } },
signal,
})
if (error) return handleError(error)
return data.data.map((instance) => parseComputeInstance(instance))
}
export type ComputeData = Awaited<ReturnType<typeof getComputeInstances>>
export const computeQueryOptions = ({ projectRef }: ComputeVariables) =>
queryOptions({
queryKey: computeKeys.list(projectRef),
queryFn: ({ signal }) => getComputeInstances({ projectRef }, signal),
enabled: IS_PLATFORM && typeof projectRef !== 'undefined',
refetchOnWindowFocus: 'always',
})
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { parseWorker } from './workers.utils'
import { parseComputeInstance } from './compute.utils'
const datum = (attributes: Record<string, unknown>) => ({
id: 'embed',
@@ -13,10 +13,10 @@ const datum = (attributes: Record<string, unknown>) => ({
},
})
describe('parseWorker', () => {
describe('parseComputeInstance', () => {
it('maps the API response onto the view model', () => {
expect(
parseWorker(
parseComputeInstance(
datum({
spec: { exposure: 'public', instances: 3, runtime: 'node', size: '4gb-2vcpu' },
instances: { declared: 3, live: 3, ready: 2, stale: 0 },
@@ -40,21 +40,26 @@ describe('parseWorker', () => {
it('treats any exposure other than public as private', () => {
expect(
parseWorker(datum({ spec: { exposure: 'internal', instances: 1, size: '2gb-1vcpu' } }))
parseComputeInstance(
datum({ spec: { exposure: 'internal', instances: 1, size: '2gb-1vcpu' } })
)
).toHaveProperty('access', 'private')
})
it('defaults deleting to false when the API omits it', () => {
expect(parseWorker(datum({}))).toHaveProperty('isDeleting', false)
expect(parseWorker(datum({ deleting: true }))).toHaveProperty('isDeleting', true)
expect(parseComputeInstance(datum({}))).toHaveProperty('isDeleting', false)
expect(parseComputeInstance(datum({ deleting: true }))).toHaveProperty('isDeleting', true)
})
it('falls back to failed for a build state it does not know', () => {
expect(parseWorker(datum({ build_state: 'exploded' }))).toHaveProperty('buildState', 'failed')
expect(parseComputeInstance(datum({ build_state: 'exploded' }))).toHaveProperty(
'buildState',
'failed'
)
})
it('rejects a response missing the fields the UI renders', () => {
expect(() => parseWorker({ id: 'embed', attributes: {} })).toThrow()
expect(() => parseWorker(undefined)).toThrow()
expect(() => parseComputeInstance({ id: 'embed', attributes: {} })).toThrow()
expect(() => parseComputeInstance(undefined)).toThrow()
})
})
@@ -1,10 +1,17 @@
import { z } from 'zod'
import type { Worker, WorkerBuildState } from '@/components/interfaces/Workers/Workers.types'
import type {
ComputeInstance,
ComputeInstanceBuildState,
} from '@/components/interfaces/Compute/Compute.types'
const BUILD_STATES = ['building', 'active', 'failed'] as const satisfies readonly WorkerBuildState[]
const BUILD_STATES = [
'building',
'active',
'failed',
] as const satisfies readonly ComputeInstanceBuildState[]
const WorkerResponseSchema = z.object({
const ComputeInstanceResponseSchema = z.object({
id: z.string(),
attributes: z.object({
build_state: z.enum(BUILD_STATES).catch('failed'),
@@ -29,8 +36,8 @@ const WorkerResponseSchema = z.object({
}),
})
export const parseWorker = (datum: unknown): Worker => {
const { id, attributes } = WorkerResponseSchema.parse(datum)
export const parseComputeInstance = (datum: unknown): ComputeInstance => {
const { id, attributes } = ComputeInstanceResponseSchema.parse(datum)
return {
name: id,
buildState: attributes.build_state,
@@ -1,7 +1,7 @@
export const workersKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'workers'] as const,
export const computeKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'compute'] as const,
detail: (projectRef: string | undefined, name: string | undefined) =>
['projects', projectRef, 'worker', name, 'detail'] as const,
['projects', projectRef, 'instance', name, 'detail'] as const,
logs: (
projectRef: string | undefined,
name: string | undefined,
@@ -11,5 +11,5 @@ export const workersKeys = {
iso_timestamp_end?: string
message?: string
}
) => ['projects', projectRef, 'worker', name, 'logs', stream, filters] as const,
) => ['projects', projectRef, 'instance', name, 'logs', stream, filters] as const,
}
@@ -170,12 +170,12 @@ describe('extractLogMetadata', () => {
})
})
describe('workers logs', () => {
describe('compute logs', () => {
it('does not synthesize unsupported request metadata', () => {
const row = {
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
log_type: 'workers',
log_type: 'compute',
metadata: {
cw_event_id: '39883203917805946105278943454814281535421893832620638214',
launch_id: '1788424715503435269',
@@ -191,7 +191,7 @@ describe('extractLogMetadata', () => {
expect(extractLogMetadata(row)).toEqual({ status: null, method: null, pathname: null })
})
it('preserves only Workers metadata while keeping unsupported fields null', () => {
it('preserves only Compute metadata while keeping unsupported fields null', () => {
const metadata = {
cw_event_id: '39883203917805946105278943454814281535421893832620638214',
launch_id: '1788424715503435269',
@@ -203,7 +203,7 @@ describe('extractLogMetadata', () => {
const mapped = mapUnifiedLogRow({
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
log_type: 'workers',
log_type: 'compute',
metadata,
timestamp: 1788424716876000,
status: 200,
@@ -229,7 +229,7 @@ describe('extractLogMetadata', () => {
expect(mapped).not.toHaveProperty('project')
})
it('does not add metadata to non-Workers rows', () => {
it('does not add metadata to non-Compute rows', () => {
const mapped = mapUnifiedLogRow({
id: 'edge-log',
timestamp: 1788424716876000,
@@ -251,7 +251,7 @@ describe('extractLogMetadata', () => {
})
describe('parseUnifiedLogsQueryRows', () => {
const workersRow = {
const computeRow = {
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
metadata: {
@@ -264,7 +264,7 @@ describe('parseUnifiedLogsQueryRows', () => {
},
project: 'cxkpapyhaaywrtudnqpl',
timestamp: 1788424716876000,
log_type: 'workers',
log_type: 'compute',
status: null,
level: null,
pathname: null,
@@ -274,15 +274,15 @@ describe('parseUnifiedLogsQueryRows', () => {
auth_user: null,
}
it('parses the Workers projection and strips project', () => {
const [parsed] = parseUnifiedLogsQueryRows([workersRow])
it('parses the Compute projection and strips project', () => {
const [parsed] = parseUnifiedLogsQueryRows([computeRow])
expect(parsed).toMatchObject({
id: workersRow.id,
timestamp: workersRow.timestamp,
event_message: workersRow.event_message,
metadata: workersRow.metadata,
log_type: 'workers',
id: computeRow.id,
timestamp: computeRow.timestamp,
event_message: computeRow.event_message,
metadata: computeRow.metadata,
log_type: 'compute',
status: null,
level: null,
pathname: null,
@@ -296,14 +296,14 @@ describe('parseUnifiedLogsQueryRows', () => {
})
it('rejects invalid metadata', () => {
expect(() => parseUnifiedLogsQueryRows([{ ...workersRow, metadata: 'invalid' }])).toThrow()
expect(() => parseUnifiedLogsQueryRows([{ ...computeRow, metadata: 'invalid' }])).toThrow()
})
it.each([{ id: 42 }, { timestamp: true }])('rejects invalid identity fields', (invalidFields) => {
expect(() => parseUnifiedLogsQueryRows([{ ...workersRow, ...invalidFields }])).toThrow()
expect(() => parseUnifiedLogsQueryRows([{ ...computeRow, ...invalidFields }])).toThrow()
})
it('rejects invalid projected field types', () => {
expect(() => parseUnifiedLogsQueryRows([{ ...workersRow, level: 'info' }])).toThrow()
expect(() => parseUnifiedLogsQueryRows([{ ...computeRow, level: 'info' }])).toThrow()
})
})
+5 -5
View File
@@ -38,7 +38,7 @@ const extractLeadingStatus = (s?: string) => {
}
export const extractLogMetadata = (row: UnifiedLogMetadataRow) => {
if (row.log_type === 'workers') {
if (row.log_type === 'compute') {
return { status: null, method: null, pathname: null }
}
@@ -58,7 +58,7 @@ export const extractLogMetadata = (row: UnifiedLogMetadataRow) => {
}
export const mapUnifiedLogRow = (row: UnifiedLogsQueryRow) => {
const isWorkersLog = row.log_type === 'workers'
const isComputeLog = row.log_type === 'compute'
const { status, method, pathname } = extractLogMetadata(row)
const mappedRow = {
@@ -68,14 +68,14 @@ export const mapUnifiedLogRow = (row: UnifiedLogsQueryRow) => {
pathname,
status,
timestamp: row.timestamp,
level: isWorkersLog ? null : row.level,
level: isComputeLog ? null : row.level,
event_message: row.event_message ?? '',
log_type: row.log_type,
log_count: row.log_count ?? null,
logs: row.logs ?? [],
auth_user: isWorkersLog ? null : row.auth_user || null,
auth_user: isComputeLog ? null : row.auth_user || null,
}
if (isWorkersLog) return { ...mappedRow, metadata: row.metadata ?? null }
if (isComputeLog) return { ...mappedRow, metadata: row.metadata ?? null }
return mappedRow
}
-32
View File
@@ -1,32 +0,0 @@
import { queryOptions } from '@tanstack/react-query'
import { workersKeys } from './keys'
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
async function getWorker({ projectRef, name }: WorkerVariables, signal?: AbortSignal) {
if (!projectRef) throw new Error('projectRef is required')
if (!name) throw new Error('name is required')
const { data, error } = await get('/v2/projects/{ref}/workers/{name}', {
params: { path: { ref: projectRef, name } },
signal,
})
if (error) return handleError(error)
return parseWorker(data.data)
}
export type WorkerData = Awaited<ReturnType<typeof getWorker>>
export const workerQueryOptions = ({ projectRef, name }: WorkerVariables) =>
queryOptions({
queryKey: workersKeys.detail(projectRef, name),
queryFn: ({ signal }) => getWorker({ projectRef, name }, signal),
enabled: IS_PLATFORM && typeof projectRef !== 'undefined' && typeof name !== 'undefined',
})
-32
View File
@@ -1,32 +0,0 @@
import { queryOptions } from '@tanstack/react-query'
import { workersKeys } from './keys'
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
async function getWorkers({ projectRef }: WorkersVariables, signal?: AbortSignal) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await get('/v2/projects/{ref}/workers', {
params: { path: { ref: projectRef } },
signal,
})
if (error) return handleError(error)
return data.data.map((worker) => parseWorker(worker))
}
export type WorkersData = Awaited<ReturnType<typeof getWorkers>>
export const workersQueryOptions = ({ projectRef }: WorkersVariables) =>
queryOptions({
queryKey: workersKeys.list(projectRef),
queryFn: ({ signal }) => getWorkers({ projectRef }, signal),
enabled: IS_PLATFORM && typeof projectRef !== 'undefined',
refetchOnWindowFocus: 'always',
})
+10
View File
@@ -0,0 +1,10 @@
export const PRODUCT_NAME = 'Compute'
export const CLI_NAME = 'compute'
// The backend still tags these rows' `log_attributes` with the pre-rename "worker" source
// names, so these values must stay as-is until the backend renames its log sources too.
export const WORKER_LOG_SOURCES = {
requests: 'worker_ingress_logs',
output: 'worker_guest_logs',
builds: 'worker_api_logs',
} as const
-8
View File
@@ -1,8 +0,0 @@
export const PRODUCT_NAME = 'Workers'
export const CLI_NAME = 'workers'
export const WORKER_LOG_SOURCES = {
requests: 'worker_ingress_logs',
output: 'worker_guest_logs',
builds: 'worker_api_logs',
} as const
@@ -0,0 +1,14 @@
import { ComputeInstanceDetail } from '@/components/interfaces/Compute/ComputeInstanceDetail/ComputeInstanceDetail'
import { ComputeLayout } from '@/components/layouts/ComputeLayout/ComputeLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import type { NextPageWithLayout } from '@/types'
const ComputeInstanceDetailPage: NextPageWithLayout = () => <ComputeInstanceDetail />
ComputeInstanceDetailPage.getLayout = (page) => (
<DefaultLayout>
<ComputeLayout title="Instance">{page}</ComputeLayout>
</DefaultLayout>
)
export default ComputeInstanceDetailPage
@@ -15,37 +15,37 @@ import {
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { DeployWorkerDialog } from '@/components/interfaces/Workers/DeployWorkerDialog'
import {
isWorkersForbidden,
isWorkersUnavailable,
} from '@/components/interfaces/Workers/Workers.utils'
import { WorkersEmptyState } from '@/components/interfaces/Workers/WorkersEmptyState'
import { WorkersList } from '@/components/interfaces/Workers/WorkersList'
isComputeForbidden,
isComputeUnavailable,
} from '@/components/interfaces/Compute/Compute.utils'
import { ComputeEmptyState } from '@/components/interfaces/Compute/ComputeEmptyState'
import { ComputeList } from '@/components/interfaces/Compute/ComputeList'
import { DeployComputeInstanceDialog } from '@/components/interfaces/Compute/DeployComputeInstanceDialog'
import { ComputeLayout } from '@/components/layouts/ComputeLayout/ComputeLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { WorkersLayout } from '@/components/layouts/WorkersLayout/WorkersLayout'
import { AlertError } from '@/components/ui/AlertError'
import { AlphaNotice } from '@/components/ui/AlphaNotice'
import { NoPermission } from '@/components/ui/NoPermission'
import { workersQueryOptions } from '@/data/workers/workers-query'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import { computeQueryOptions } from '@/data/compute/compute-query'
import { PRODUCT_NAME } from '@/lib/constants/compute'
import type { NextPageWithLayout } from '@/types'
const WorkersPage: NextPageWithLayout = () => {
const ComputePage: NextPageWithLayout = () => {
const { ref } = useParams()
const [isDeployInstructionsOpen, setIsDeployInstructionsOpen] = useState(false)
const {
data: workers,
data: instances,
error,
isPending,
isError,
isSuccess,
isFetching,
refetch,
} = useQuery(workersQueryOptions({ projectRef: ref }))
} = useQuery(computeQueryOptions({ projectRef: ref }))
const isNotEnrolled = isError && isWorkersUnavailable(error)
const isMissingPermission = isError && isWorkersForbidden(error)
const isNotEnrolled = isError && isComputeUnavailable(error)
const isMissingPermission = isError && isComputeForbidden(error)
const isUnexpectedError = isError && !isNotEnrolled && !isMissingPermission
return (
@@ -65,7 +65,7 @@ const WorkersPage: NextPageWithLayout = () => {
<PageSection>
<PageSectionContent className="flex flex-col gap-y-8">
<AlphaNotice
entity="Workers"
entity="Compute"
feedbackUrl="https://github.com/orgs/supabase/discussions"
/>
@@ -77,11 +77,13 @@ const WorkersPage: NextPageWithLayout = () => {
description={`${PRODUCT_NAME} is in Private Alpha. Contact support to have this project added to the alpha.`}
/>
)}
{isMissingPermission && <NoPermission resourceText="view this project's workers" />}
{isMissingPermission && (
<NoPermission resourceText="view this project's compute instances" />
)}
{isUnexpectedError && (
<AlertError
error={error}
subject="Failed to retrieve workers"
subject="Failed to retrieve compute instances"
additionalActions={
<Button
variant="default"
@@ -94,13 +96,13 @@ const WorkersPage: NextPageWithLayout = () => {
}
/>
)}
{isSuccess && workers.length === 0 && (
<WorkersEmptyState onDeploy={() => setIsDeployInstructionsOpen(true)} />
{isSuccess && instances.length === 0 && (
<ComputeEmptyState onDeploy={() => setIsDeployInstructionsOpen(true)} />
)}
{isSuccess && workers.length > 0 && ref && (
<WorkersList
{isSuccess && instances.length > 0 && ref && (
<ComputeList
projectRef={ref}
workers={workers}
instances={instances}
onDeploy={() => setIsDeployInstructionsOpen(true)}
onRefresh={() => refetch()}
isRefreshing={isFetching}
@@ -110,7 +112,7 @@ const WorkersPage: NextPageWithLayout = () => {
</PageSection>
</PageContainer>
<DeployWorkerDialog
<DeployComputeInstanceDialog
open={isDeployInstructionsOpen}
onOpenChange={setIsDeployInstructionsOpen}
/>
@@ -118,10 +120,10 @@ const WorkersPage: NextPageWithLayout = () => {
)
}
WorkersPage.getLayout = (page) => (
ComputePage.getLayout = (page) => (
<DefaultLayout>
<WorkersLayout title={PRODUCT_NAME}>{page}</WorkersLayout>
<ComputeLayout title={PRODUCT_NAME}>{page}</ComputeLayout>
</DefaultLayout>
)
export default WorkersPage
export default ComputePage
@@ -13,14 +13,14 @@ import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { DefaultEdgeFunctionSecrets } from '@/components/interfaces/Functions/EdgeFunctionSecrets/DefaultEdgeFunctionSecrets'
import { DEFAULT_EDGE_FUNCTION_SECRETS } from '@/components/interfaces/Functions/EdgeFunctionSecrets/DefaultEdgeFunctionSecrets.utils'
import { EdgeFunctionSecrets } from '@/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets'
import { ComputeLayout } from '@/components/layouts/ComputeLayout/ComputeLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { WorkersLayout } from '@/components/layouts/WorkersLayout/WorkersLayout'
import { DocsButton } from '@/components/ui/DocsButton'
import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode'
import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
const WorkerSecretsPage: NextPageWithLayout = () => {
const ComputeSecretsPage: NextPageWithLayout = () => {
const { isCli, isSelfHosted } = useDeploymentMode()
if (!IS_PLATFORM) {
@@ -68,7 +68,7 @@ const WorkerSecretsPage: NextPageWithLayout = () => {
<PageSectionContent className="space-y-4 md:space-y-8">
<Admonition
type="default"
title="Secrets are shared between Edge Functions and Workers"
title="Secrets are shared between Edge Functions and Compute"
/>
<EdgeFunctionSecrets />
</PageSectionContent>
@@ -78,14 +78,14 @@ const WorkerSecretsPage: NextPageWithLayout = () => {
}
// Hoisted out of `getLayout` so the TanStack route can import it directly.
export const WorkerSecretsPageWrapper = ({ children }: PropsWithChildren) => (
export const ComputeSecretsPageWrapper = ({ children }: PropsWithChildren) => (
<div className="w-full min-h-full flex flex-col items-stretch">
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Secrets</PageHeaderTitle>
<PageHeaderDescription>
Environment variables loaded into every worker at start-up
Environment variables loaded into every instance at start-up
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
@@ -95,12 +95,12 @@ export const WorkerSecretsPageWrapper = ({ children }: PropsWithChildren) => (
</div>
)
WorkerSecretsPage.getLayout = (page) => (
ComputeSecretsPage.getLayout = (page) => (
<DefaultLayout>
<WorkersLayout title="Secrets">
<WorkerSecretsPageWrapper>{page}</WorkerSecretsPageWrapper>
</WorkersLayout>
<ComputeLayout title="Secrets">
<ComputeSecretsPageWrapper>{page}</ComputeSecretsPageWrapper>
</ComputeLayout>
</DefaultLayout>
)
export default WorkerSecretsPage
export default ComputeSecretsPage
@@ -1,14 +0,0 @@
import { WorkerDetail } from '@/components/interfaces/Workers/WorkerDetail/WorkerDetail'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { WorkersLayout } from '@/components/layouts/WorkersLayout/WorkersLayout'
import type { NextPageWithLayout } from '@/types'
const WorkerDetailPage: NextPageWithLayout = () => <WorkerDetail />
WorkerDetailPage.getLayout = (page) => (
<DefaultLayout>
<WorkersLayout title="Worker">{page}</WorkersLayout>
</DefaultLayout>
)
export default WorkerDetailPage
+92 -92
View File
@@ -56,7 +56,6 @@ import { Route as ApiConnectIndexRouteImport } from './routes/api/connect/index'
import { Route as AppOrgIndexRouteImport } from './routes/_app/org/index'
import { Route as AppNewIndexRouteImport } from './routes/_app/new/index'
import { Route as ProjectChar91_Char93SplatRouteImport } from './routes/project.[_].$'
import { Route as ProjectRefWorkersRouteImport } from './routes/project/$ref/workers'
import { Route as ProjectRefStorageRouteImport } from './routes/project/$ref/storage'
import { Route as ProjectRefSqlRouteImport } from './routes/project/$ref/sql'
import { Route as ProjectRefSettingsRouteImport } from './routes/project/$ref/settings'
@@ -69,6 +68,7 @@ import { Route as ProjectRefFunctionsRouteImport } from './routes/project/$ref/f
import { Route as ProjectRefExplorerRouteImport } from './routes/project/$ref/explorer'
import { Route as ProjectRefEditorRouteImport } from './routes/project/$ref/editor'
import { Route as ProjectRefDatabaseRouteImport } from './routes/project/$ref/database'
import { Route as ProjectRefComputeRouteImport } from './routes/project/$ref/compute'
import { Route as ProjectRefBranchesRouteImport } from './routes/project/$ref/branches'
import { Route as ProjectRefAuthRouteImport } from './routes/project/$ref/auth'
import { Route as ProjectRefAdvisorsRouteImport } from './routes/project/$ref/advisors'
@@ -86,7 +86,6 @@ import { Route as AppSupportLinkRouteImport } from './routes/_app/support/link'
import { Route as AppAccountSecurityRouteImport } from './routes/_app/account/security'
import { Route as AppAccountMeRouteImport } from './routes/_app/account/me'
import { Route as AppAccountAuditRouteImport } from './routes/_app/account/audit'
import { Route as ProjectRefWorkersIndexRouteImport } from './routes/project/$ref/workers/index'
import { Route as ProjectRefSqlIndexRouteImport } from './routes/project/$ref/sql/index'
import { Route as ProjectRefObservabilityIndexRouteImport } from './routes/project/$ref/observability/index'
import { Route as ProjectRefLogsIndexRouteImport } from './routes/project/$ref/logs/index'
@@ -94,6 +93,7 @@ import { Route as ProjectRefIntegrationsIndexRouteImport } from './routes/projec
import { Route as ProjectRefFunctionsIndexRouteImport } from './routes/project/$ref/functions/index'
import { Route as ProjectRefExplorerIndexRouteImport } from './routes/project/$ref/explorer/index'
import { Route as ProjectRefEditorIndexRouteImport } from './routes/project/$ref/editor/index'
import { Route as ProjectRefComputeIndexRouteImport } from './routes/project/$ref/compute/index'
import { Route as ProjectRefBranchesIndexRouteImport } from './routes/project/$ref/branches/index'
import { Route as ProjectRefApiIndexRouteImport } from './routes/project/$ref/api/index'
import { Route as ApiPlatformProjectsIndexRouteImport } from './routes/api/platform/projects/index'
@@ -101,8 +101,6 @@ import { Route as ApiPlatformProfileIndexRouteImport } from './routes/api/platfo
import { Route as ApiPlatformOrganizationsIndexRouteImport } from './routes/api/platform/organizations/index'
import { Route as AppOrgSlugIndexRouteImport } from './routes/_app/org/$slug/index'
import { Route as AppAccountTokensIndexRouteImport } from './routes/_app/account/tokens/index'
import { Route as ProjectRefWorkersSecretsRouteImport } from './routes/project/$ref/workers/secrets'
import { Route as ProjectRefWorkersNameRouteImport } from './routes/project/$ref/workers/$name'
import { Route as ProjectRefStorageS3RouteImport } from './routes/project/$ref/storage/s3'
import { Route as ProjectRefSqlTemplatesRouteImport } from './routes/project/$ref/sql/templates'
import { Route as ProjectRefSqlExamplesRouteImport } from './routes/project/$ref/sql/examples'
@@ -159,6 +157,8 @@ import { Route as ProjectRefDatabaseIndexesRouteImport } from './routes/project/
import { Route as ProjectRefDatabaseFunctionsRouteImport } from './routes/project/$ref/database/functions'
import { Route as ProjectRefDatabaseExtensionsRouteImport } from './routes/project/$ref/database/extensions'
import { Route as ProjectRefDatabaseColumnPrivilegesRouteImport } from './routes/project/$ref/database/column-privileges'
import { Route as ProjectRefComputeSecretsRouteImport } from './routes/project/$ref/compute/secrets'
import { Route as ProjectRefComputeNameRouteImport } from './routes/project/$ref/compute/$name'
import { Route as ProjectRefBranchesMergeRequestsRouteImport } from './routes/project/$ref/branches/merge-requests'
import { Route as ProjectRefAuthUsersRouteImport } from './routes/project/$ref/auth/users'
import { Route as ProjectRefAuthUrlConfigurationRouteImport } from './routes/project/$ref/auth/url-configuration'
@@ -568,11 +568,6 @@ const ProjectChar91_Char93SplatRoute =
path: '/$',
getParentRoute: () => ProjectChar91_Char93Route,
} as any)
const ProjectRefWorkersRoute = ProjectRefWorkersRouteImport.update({
id: '/workers',
path: '/workers',
getParentRoute: () => ProjectRefRoute,
} as any)
const ProjectRefStorageRoute = ProjectRefStorageRouteImport.update({
id: '/storage',
path: '/storage',
@@ -633,6 +628,11 @@ const ProjectRefDatabaseRoute = ProjectRefDatabaseRouteImport.update({
path: '/database',
getParentRoute: () => ProjectRefRoute,
} as any)
const ProjectRefComputeRoute = ProjectRefComputeRouteImport.update({
id: '/compute',
path: '/compute',
getParentRoute: () => ProjectRefRoute,
} as any)
const ProjectRefBranchesRoute = ProjectRefBranchesRouteImport.update({
id: '/branches',
path: '/branches',
@@ -722,11 +722,6 @@ const AppAccountAuditRoute = AppAccountAuditRouteImport.update({
path: '/audit',
getParentRoute: () => AppAccountRoute,
} as any)
const ProjectRefWorkersIndexRoute = ProjectRefWorkersIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => ProjectRefWorkersRoute,
} as any)
const ProjectRefSqlIndexRoute = ProjectRefSqlIndexRouteImport.update({
id: '/',
path: '/',
@@ -765,6 +760,11 @@ const ProjectRefEditorIndexRoute = ProjectRefEditorIndexRouteImport.update({
path: '/',
getParentRoute: () => ProjectRefEditorRoute,
} as any)
const ProjectRefComputeIndexRoute = ProjectRefComputeIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => ProjectRefComputeRoute,
} as any)
const ProjectRefBranchesIndexRoute = ProjectRefBranchesIndexRouteImport.update({
id: '/',
path: '/',
@@ -802,17 +802,6 @@ const AppAccountTokensIndexRoute = AppAccountTokensIndexRouteImport.update({
path: '/tokens/',
getParentRoute: () => AppAccountRoute,
} as any)
const ProjectRefWorkersSecretsRoute =
ProjectRefWorkersSecretsRouteImport.update({
id: '/secrets',
path: '/secrets',
getParentRoute: () => ProjectRefWorkersRoute,
} as any)
const ProjectRefWorkersNameRoute = ProjectRefWorkersNameRouteImport.update({
id: '/$name',
path: '/$name',
getParentRoute: () => ProjectRefWorkersRoute,
} as any)
const ProjectRefStorageS3Route = ProjectRefStorageS3RouteImport.update({
id: '/s3',
path: '/s3',
@@ -1136,6 +1125,17 @@ const ProjectRefDatabaseColumnPrivilegesRoute =
path: '/column-privileges',
getParentRoute: () => ProjectRefDatabaseRoute,
} as any)
const ProjectRefComputeSecretsRoute =
ProjectRefComputeSecretsRouteImport.update({
id: '/secrets',
path: '/secrets',
getParentRoute: () => ProjectRefComputeRoute,
} as any)
const ProjectRefComputeNameRoute = ProjectRefComputeNameRouteImport.update({
id: '/$name',
path: '/$name',
getParentRoute: () => ProjectRefComputeRoute,
} as any)
const ProjectRefBranchesMergeRequestsRoute =
ProjectRefBranchesMergeRequestsRouteImport.update({
id: '/merge-requests',
@@ -2177,6 +2177,7 @@ export interface FileRoutesByFullPath {
'/project/$ref/advisors': typeof ProjectRefAdvisorsRouteWithChildren
'/project/$ref/auth': typeof ProjectRefAuthRouteWithChildren
'/project/$ref/branches': typeof ProjectRefBranchesRouteWithChildren
'/project/$ref/compute': typeof ProjectRefComputeRouteWithChildren
'/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren
'/project/$ref/editor': typeof ProjectRefEditorRouteWithChildren
'/project/$ref/explorer': typeof ProjectRefExplorerRouteWithChildren
@@ -2189,7 +2190,6 @@ export interface FileRoutesByFullPath {
'/project/$ref/settings': typeof ProjectRefSettingsRouteWithChildren
'/project/$ref/sql': typeof ProjectRefSqlRouteWithChildren
'/project/$ref/storage': typeof ProjectRefStorageRouteWithChildren
'/project/$ref/workers': typeof ProjectRefWorkersRouteWithChildren
'/project/_/$': typeof ProjectChar91_Char93SplatRoute
'/new/': typeof AppNewIndexRoute
'/org/': typeof AppOrgIndexRoute
@@ -2242,6 +2242,8 @@ export interface FileRoutesByFullPath {
'/project/$ref/auth/url-configuration': typeof ProjectRefAuthUrlConfigurationRoute
'/project/$ref/auth/users': typeof ProjectRefAuthUsersRoute
'/project/$ref/branches/merge-requests': typeof ProjectRefBranchesMergeRequestsRoute
'/project/$ref/compute/$name': typeof ProjectRefComputeNameRoute
'/project/$ref/compute/secrets': typeof ProjectRefComputeSecretsRoute
'/project/$ref/database/column-privileges': typeof ProjectRefDatabaseColumnPrivilegesRoute
'/project/$ref/database/extensions': typeof ProjectRefDatabaseExtensionsRoute
'/project/$ref/database/functions': typeof ProjectRefDatabaseFunctionsRoute
@@ -2298,8 +2300,6 @@ export interface FileRoutesByFullPath {
'/project/$ref/sql/examples': typeof ProjectRefSqlExamplesRoute
'/project/$ref/sql/templates': typeof ProjectRefSqlTemplatesRoute
'/project/$ref/storage/s3': typeof ProjectRefStorageS3Route
'/project/$ref/workers/$name': typeof ProjectRefWorkersNameRoute
'/project/$ref/workers/secrets': typeof ProjectRefWorkersSecretsRoute
'/account/tokens/': typeof AppAccountTokensIndexRoute
'/org/$slug/': typeof AppOrgSlugIndexRoute
'/api/platform/organizations/': typeof ApiPlatformOrganizationsIndexRoute
@@ -2307,6 +2307,7 @@ export interface FileRoutesByFullPath {
'/api/platform/projects/': typeof ApiPlatformProjectsIndexRoute
'/project/$ref/api/': typeof ProjectRefApiIndexRoute
'/project/$ref/branches/': typeof ProjectRefBranchesIndexRoute
'/project/$ref/compute/': typeof ProjectRefComputeIndexRoute
'/project/$ref/editor/': typeof ProjectRefEditorIndexRoute
'/project/$ref/explorer/': typeof ProjectRefExplorerIndexRoute
'/project/$ref/functions/': typeof ProjectRefFunctionsIndexRoute
@@ -2314,7 +2315,6 @@ export interface FileRoutesByFullPath {
'/project/$ref/logs/': typeof ProjectRefLogsIndexRoute
'/project/$ref/observability/': typeof ProjectRefObservabilityIndexRoute
'/project/$ref/sql/': typeof ProjectRefSqlIndexRoute
'/project/$ref/workers/': typeof ProjectRefWorkersIndexRoute
'/org/$slug/webhooks/$endpointId': typeof AppOrgSlugWebhooksEndpointIdRoute
'/partners/stripe/projects/login': typeof AuthPartnersStripeProjectsLoginRoute
'/api/platform/auth/$ref/invite': typeof ApiPlatformAuthRefInviteRoute
@@ -2551,6 +2551,8 @@ export interface FileRoutesByTo {
'/project/$ref/auth/url-configuration': typeof ProjectRefAuthUrlConfigurationRoute
'/project/$ref/auth/users': typeof ProjectRefAuthUsersRoute
'/project/$ref/branches/merge-requests': typeof ProjectRefBranchesMergeRequestsRoute
'/project/$ref/compute/$name': typeof ProjectRefComputeNameRoute
'/project/$ref/compute/secrets': typeof ProjectRefComputeSecretsRoute
'/project/$ref/database/column-privileges': typeof ProjectRefDatabaseColumnPrivilegesRoute
'/project/$ref/database/extensions': typeof ProjectRefDatabaseExtensionsRoute
'/project/$ref/database/functions': typeof ProjectRefDatabaseFunctionsRoute
@@ -2604,8 +2606,6 @@ export interface FileRoutesByTo {
'/project/$ref/sql/examples': typeof ProjectRefSqlExamplesRoute
'/project/$ref/sql/templates': typeof ProjectRefSqlTemplatesRoute
'/project/$ref/storage/s3': typeof ProjectRefStorageS3Route
'/project/$ref/workers/$name': typeof ProjectRefWorkersNameRoute
'/project/$ref/workers/secrets': typeof ProjectRefWorkersSecretsRoute
'/account/tokens': typeof AppAccountTokensIndexRoute
'/org/$slug': typeof AppOrgSlugIndexRoute
'/api/platform/organizations': typeof ApiPlatformOrganizationsIndexRoute
@@ -2613,6 +2613,7 @@ export interface FileRoutesByTo {
'/api/platform/projects': typeof ApiPlatformProjectsIndexRoute
'/project/$ref/api': typeof ProjectRefApiIndexRoute
'/project/$ref/branches': typeof ProjectRefBranchesIndexRoute
'/project/$ref/compute': typeof ProjectRefComputeIndexRoute
'/project/$ref/editor': typeof ProjectRefEditorIndexRoute
'/project/$ref/explorer': typeof ProjectRefExplorerIndexRoute
'/project/$ref/functions': typeof ProjectRefFunctionsIndexRoute
@@ -2620,7 +2621,6 @@ export interface FileRoutesByTo {
'/project/$ref/logs': typeof ProjectRefLogsIndexRoute
'/project/$ref/observability': typeof ProjectRefObservabilityIndexRoute
'/project/$ref/sql': typeof ProjectRefSqlIndexRoute
'/project/$ref/workers': typeof ProjectRefWorkersIndexRoute
'/org/$slug/webhooks/$endpointId': typeof AppOrgSlugWebhooksEndpointIdRoute
'/partners/stripe/projects/login': typeof AuthPartnersStripeProjectsLoginRoute
'/api/platform/auth/$ref/invite': typeof ApiPlatformAuthRefInviteRoute
@@ -2806,6 +2806,7 @@ export interface FileRoutesById {
'/project/$ref/advisors': typeof ProjectRefAdvisorsRouteWithChildren
'/project/$ref/auth': typeof ProjectRefAuthRouteWithChildren
'/project/$ref/branches': typeof ProjectRefBranchesRouteWithChildren
'/project/$ref/compute': typeof ProjectRefComputeRouteWithChildren
'/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren
'/project/$ref/editor': typeof ProjectRefEditorRouteWithChildren
'/project/$ref/explorer': typeof ProjectRefExplorerRouteWithChildren
@@ -2818,7 +2819,6 @@ export interface FileRoutesById {
'/project/$ref/settings': typeof ProjectRefSettingsRouteWithChildren
'/project/$ref/sql': typeof ProjectRefSqlRouteWithChildren
'/project/$ref/storage': typeof ProjectRefStorageRouteWithChildren
'/project/$ref/workers': typeof ProjectRefWorkersRouteWithChildren
'/project/_/$': typeof ProjectChar91_Char93SplatRoute
'/_app/new/': typeof AppNewIndexRoute
'/_app/org/': typeof AppOrgIndexRoute
@@ -2871,6 +2871,8 @@ export interface FileRoutesById {
'/project/$ref/auth/url-configuration': typeof ProjectRefAuthUrlConfigurationRoute
'/project/$ref/auth/users': typeof ProjectRefAuthUsersRoute
'/project/$ref/branches/merge-requests': typeof ProjectRefBranchesMergeRequestsRoute
'/project/$ref/compute/$name': typeof ProjectRefComputeNameRoute
'/project/$ref/compute/secrets': typeof ProjectRefComputeSecretsRoute
'/project/$ref/database/column-privileges': typeof ProjectRefDatabaseColumnPrivilegesRoute
'/project/$ref/database/extensions': typeof ProjectRefDatabaseExtensionsRoute
'/project/$ref/database/functions': typeof ProjectRefDatabaseFunctionsRoute
@@ -2927,8 +2929,6 @@ export interface FileRoutesById {
'/project/$ref/sql/examples': typeof ProjectRefSqlExamplesRoute
'/project/$ref/sql/templates': typeof ProjectRefSqlTemplatesRoute
'/project/$ref/storage/s3': typeof ProjectRefStorageS3Route
'/project/$ref/workers/$name': typeof ProjectRefWorkersNameRoute
'/project/$ref/workers/secrets': typeof ProjectRefWorkersSecretsRoute
'/_app/account/tokens/': typeof AppAccountTokensIndexRoute
'/_app/org/$slug/': typeof AppOrgSlugIndexRoute
'/api/platform/organizations/': typeof ApiPlatformOrganizationsIndexRoute
@@ -2936,6 +2936,7 @@ export interface FileRoutesById {
'/api/platform/projects/': typeof ApiPlatformProjectsIndexRoute
'/project/$ref/api/': typeof ProjectRefApiIndexRoute
'/project/$ref/branches/': typeof ProjectRefBranchesIndexRoute
'/project/$ref/compute/': typeof ProjectRefComputeIndexRoute
'/project/$ref/editor/': typeof ProjectRefEditorIndexRoute
'/project/$ref/explorer/': typeof ProjectRefExplorerIndexRoute
'/project/$ref/functions/': typeof ProjectRefFunctionsIndexRoute
@@ -2943,7 +2944,6 @@ export interface FileRoutesById {
'/project/$ref/logs/': typeof ProjectRefLogsIndexRoute
'/project/$ref/observability/': typeof ProjectRefObservabilityIndexRoute
'/project/$ref/sql/': typeof ProjectRefSqlIndexRoute
'/project/$ref/workers/': typeof ProjectRefWorkersIndexRoute
'/_app/org/$slug/webhooks/$endpointId': typeof AppOrgSlugWebhooksEndpointIdRoute
'/_auth/partners/stripe/projects/login': typeof AuthPartnersStripeProjectsLoginRoute
'/api/platform/auth/$ref/invite': typeof ApiPlatformAuthRefInviteRoute
@@ -3128,6 +3128,7 @@ export interface FileRouteTypes {
| '/project/$ref/advisors'
| '/project/$ref/auth'
| '/project/$ref/branches'
| '/project/$ref/compute'
| '/project/$ref/database'
| '/project/$ref/editor'
| '/project/$ref/explorer'
@@ -3140,7 +3141,6 @@ export interface FileRouteTypes {
| '/project/$ref/settings'
| '/project/$ref/sql'
| '/project/$ref/storage'
| '/project/$ref/workers'
| '/project/_/$'
| '/new/'
| '/org/'
@@ -3193,6 +3193,8 @@ export interface FileRouteTypes {
| '/project/$ref/auth/url-configuration'
| '/project/$ref/auth/users'
| '/project/$ref/branches/merge-requests'
| '/project/$ref/compute/$name'
| '/project/$ref/compute/secrets'
| '/project/$ref/database/column-privileges'
| '/project/$ref/database/extensions'
| '/project/$ref/database/functions'
@@ -3249,8 +3251,6 @@ export interface FileRouteTypes {
| '/project/$ref/sql/examples'
| '/project/$ref/sql/templates'
| '/project/$ref/storage/s3'
| '/project/$ref/workers/$name'
| '/project/$ref/workers/secrets'
| '/account/tokens/'
| '/org/$slug/'
| '/api/platform/organizations/'
@@ -3258,6 +3258,7 @@ export interface FileRouteTypes {
| '/api/platform/projects/'
| '/project/$ref/api/'
| '/project/$ref/branches/'
| '/project/$ref/compute/'
| '/project/$ref/editor/'
| '/project/$ref/explorer/'
| '/project/$ref/functions/'
@@ -3265,7 +3266,6 @@ export interface FileRouteTypes {
| '/project/$ref/logs/'
| '/project/$ref/observability/'
| '/project/$ref/sql/'
| '/project/$ref/workers/'
| '/org/$slug/webhooks/$endpointId'
| '/partners/stripe/projects/login'
| '/api/platform/auth/$ref/invite'
@@ -3502,6 +3502,8 @@ export interface FileRouteTypes {
| '/project/$ref/auth/url-configuration'
| '/project/$ref/auth/users'
| '/project/$ref/branches/merge-requests'
| '/project/$ref/compute/$name'
| '/project/$ref/compute/secrets'
| '/project/$ref/database/column-privileges'
| '/project/$ref/database/extensions'
| '/project/$ref/database/functions'
@@ -3555,8 +3557,6 @@ export interface FileRouteTypes {
| '/project/$ref/sql/examples'
| '/project/$ref/sql/templates'
| '/project/$ref/storage/s3'
| '/project/$ref/workers/$name'
| '/project/$ref/workers/secrets'
| '/account/tokens'
| '/org/$slug'
| '/api/platform/organizations'
@@ -3564,6 +3564,7 @@ export interface FileRouteTypes {
| '/api/platform/projects'
| '/project/$ref/api'
| '/project/$ref/branches'
| '/project/$ref/compute'
| '/project/$ref/editor'
| '/project/$ref/explorer'
| '/project/$ref/functions'
@@ -3571,7 +3572,6 @@ export interface FileRouteTypes {
| '/project/$ref/logs'
| '/project/$ref/observability'
| '/project/$ref/sql'
| '/project/$ref/workers'
| '/org/$slug/webhooks/$endpointId'
| '/partners/stripe/projects/login'
| '/api/platform/auth/$ref/invite'
@@ -3756,6 +3756,7 @@ export interface FileRouteTypes {
| '/project/$ref/advisors'
| '/project/$ref/auth'
| '/project/$ref/branches'
| '/project/$ref/compute'
| '/project/$ref/database'
| '/project/$ref/editor'
| '/project/$ref/explorer'
@@ -3768,7 +3769,6 @@ export interface FileRouteTypes {
| '/project/$ref/settings'
| '/project/$ref/sql'
| '/project/$ref/storage'
| '/project/$ref/workers'
| '/project/_/$'
| '/_app/new/'
| '/_app/org/'
@@ -3821,6 +3821,8 @@ export interface FileRouteTypes {
| '/project/$ref/auth/url-configuration'
| '/project/$ref/auth/users'
| '/project/$ref/branches/merge-requests'
| '/project/$ref/compute/$name'
| '/project/$ref/compute/secrets'
| '/project/$ref/database/column-privileges'
| '/project/$ref/database/extensions'
| '/project/$ref/database/functions'
@@ -3877,8 +3879,6 @@ export interface FileRouteTypes {
| '/project/$ref/sql/examples'
| '/project/$ref/sql/templates'
| '/project/$ref/storage/s3'
| '/project/$ref/workers/$name'
| '/project/$ref/workers/secrets'
| '/_app/account/tokens/'
| '/_app/org/$slug/'
| '/api/platform/organizations/'
@@ -3886,6 +3886,7 @@ export interface FileRouteTypes {
| '/api/platform/projects/'
| '/project/$ref/api/'
| '/project/$ref/branches/'
| '/project/$ref/compute/'
| '/project/$ref/editor/'
| '/project/$ref/explorer/'
| '/project/$ref/functions/'
@@ -3893,7 +3894,6 @@ export interface FileRouteTypes {
| '/project/$ref/logs/'
| '/project/$ref/observability/'
| '/project/$ref/sql/'
| '/project/$ref/workers/'
| '/_app/org/$slug/webhooks/$endpointId'
| '/_auth/partners/stripe/projects/login'
| '/api/platform/auth/$ref/invite'
@@ -4475,13 +4475,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectChar91_Char93SplatRouteImport
parentRoute: typeof ProjectChar91_Char93Route
}
'/project/$ref/workers': {
id: '/project/$ref/workers'
path: '/workers'
fullPath: '/project/$ref/workers'
preLoaderRoute: typeof ProjectRefWorkersRouteImport
parentRoute: typeof ProjectRefRoute
}
'/project/$ref/storage': {
id: '/project/$ref/storage'
path: '/storage'
@@ -4566,6 +4559,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectRefDatabaseRouteImport
parentRoute: typeof ProjectRefRoute
}
'/project/$ref/compute': {
id: '/project/$ref/compute'
path: '/compute'
fullPath: '/project/$ref/compute'
preLoaderRoute: typeof ProjectRefComputeRouteImport
parentRoute: typeof ProjectRefRoute
}
'/project/$ref/branches': {
id: '/project/$ref/branches'
path: '/branches'
@@ -4685,13 +4685,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAccountAuditRouteImport
parentRoute: typeof AppAccountRoute
}
'/project/$ref/workers/': {
id: '/project/$ref/workers/'
path: '/'
fullPath: '/project/$ref/workers/'
preLoaderRoute: typeof ProjectRefWorkersIndexRouteImport
parentRoute: typeof ProjectRefWorkersRoute
}
'/project/$ref/sql/': {
id: '/project/$ref/sql/'
path: '/'
@@ -4741,6 +4734,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectRefEditorIndexRouteImport
parentRoute: typeof ProjectRefEditorRoute
}
'/project/$ref/compute/': {
id: '/project/$ref/compute/'
path: '/'
fullPath: '/project/$ref/compute/'
preLoaderRoute: typeof ProjectRefComputeIndexRouteImport
parentRoute: typeof ProjectRefComputeRoute
}
'/project/$ref/branches/': {
id: '/project/$ref/branches/'
path: '/'
@@ -4790,20 +4790,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAccountTokensIndexRouteImport
parentRoute: typeof AppAccountRoute
}
'/project/$ref/workers/secrets': {
id: '/project/$ref/workers/secrets'
path: '/secrets'
fullPath: '/project/$ref/workers/secrets'
preLoaderRoute: typeof ProjectRefWorkersSecretsRouteImport
parentRoute: typeof ProjectRefWorkersRoute
}
'/project/$ref/workers/$name': {
id: '/project/$ref/workers/$name'
path: '/$name'
fullPath: '/project/$ref/workers/$name'
preLoaderRoute: typeof ProjectRefWorkersNameRouteImport
parentRoute: typeof ProjectRefWorkersRoute
}
'/project/$ref/storage/s3': {
id: '/project/$ref/storage/s3'
path: '/s3'
@@ -5196,6 +5182,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectRefDatabaseColumnPrivilegesRouteImport
parentRoute: typeof ProjectRefDatabaseRoute
}
'/project/$ref/compute/secrets': {
id: '/project/$ref/compute/secrets'
path: '/secrets'
fullPath: '/project/$ref/compute/secrets'
preLoaderRoute: typeof ProjectRefComputeSecretsRouteImport
parentRoute: typeof ProjectRefComputeRoute
}
'/project/$ref/compute/$name': {
id: '/project/$ref/compute/$name'
path: '/$name'
fullPath: '/project/$ref/compute/$name'
preLoaderRoute: typeof ProjectRefComputeNameRouteImport
parentRoute: typeof ProjectRefComputeRoute
}
'/project/$ref/branches/merge-requests': {
id: '/project/$ref/branches/merge-requests'
path: '/merge-requests'
@@ -6620,6 +6620,21 @@ const ProjectRefBranchesRouteChildren: ProjectRefBranchesRouteChildren = {
const ProjectRefBranchesRouteWithChildren =
ProjectRefBranchesRoute._addFileChildren(ProjectRefBranchesRouteChildren)
interface ProjectRefComputeRouteChildren {
ProjectRefComputeNameRoute: typeof ProjectRefComputeNameRoute
ProjectRefComputeSecretsRoute: typeof ProjectRefComputeSecretsRoute
ProjectRefComputeIndexRoute: typeof ProjectRefComputeIndexRoute
}
const ProjectRefComputeRouteChildren: ProjectRefComputeRouteChildren = {
ProjectRefComputeNameRoute: ProjectRefComputeNameRoute,
ProjectRefComputeSecretsRoute: ProjectRefComputeSecretsRoute,
ProjectRefComputeIndexRoute: ProjectRefComputeIndexRoute,
}
const ProjectRefComputeRouteWithChildren =
ProjectRefComputeRoute._addFileChildren(ProjectRefComputeRouteChildren)
interface ProjectRefDatabaseTriggersRouteChildren {
ProjectRefDatabaseTriggersDataRoute: typeof ProjectRefDatabaseTriggersDataRoute
ProjectRefDatabaseTriggersEventRoute: typeof ProjectRefDatabaseTriggersEventRoute
@@ -7008,25 +7023,11 @@ const ProjectRefStorageRouteChildren: ProjectRefStorageRouteChildren = {
const ProjectRefStorageRouteWithChildren =
ProjectRefStorageRoute._addFileChildren(ProjectRefStorageRouteChildren)
interface ProjectRefWorkersRouteChildren {
ProjectRefWorkersNameRoute: typeof ProjectRefWorkersNameRoute
ProjectRefWorkersSecretsRoute: typeof ProjectRefWorkersSecretsRoute
ProjectRefWorkersIndexRoute: typeof ProjectRefWorkersIndexRoute
}
const ProjectRefWorkersRouteChildren: ProjectRefWorkersRouteChildren = {
ProjectRefWorkersNameRoute: ProjectRefWorkersNameRoute,
ProjectRefWorkersSecretsRoute: ProjectRefWorkersSecretsRoute,
ProjectRefWorkersIndexRoute: ProjectRefWorkersIndexRoute,
}
const ProjectRefWorkersRouteWithChildren =
ProjectRefWorkersRoute._addFileChildren(ProjectRefWorkersRouteChildren)
interface ProjectRefRouteChildren {
ProjectRefAdvisorsRoute: typeof ProjectRefAdvisorsRouteWithChildren
ProjectRefAuthRoute: typeof ProjectRefAuthRouteWithChildren
ProjectRefBranchesRoute: typeof ProjectRefBranchesRouteWithChildren
ProjectRefComputeRoute: typeof ProjectRefComputeRouteWithChildren
ProjectRefDatabaseRoute: typeof ProjectRefDatabaseRouteWithChildren
ProjectRefEditorRoute: typeof ProjectRefEditorRouteWithChildren
ProjectRefExplorerRoute: typeof ProjectRefExplorerRouteWithChildren
@@ -7039,7 +7040,6 @@ interface ProjectRefRouteChildren {
ProjectRefSettingsRoute: typeof ProjectRefSettingsRouteWithChildren
ProjectRefSqlRoute: typeof ProjectRefSqlRouteWithChildren
ProjectRefStorageRoute: typeof ProjectRefStorageRouteWithChildren
ProjectRefWorkersRoute: typeof ProjectRefWorkersRouteWithChildren
ProjectRefIndexRoute: typeof ProjectRefIndexRoute
ProjectRefApiIndexRoute: typeof ProjectRefApiIndexRoute
}
@@ -7048,6 +7048,7 @@ const ProjectRefRouteChildren: ProjectRefRouteChildren = {
ProjectRefAdvisorsRoute: ProjectRefAdvisorsRouteWithChildren,
ProjectRefAuthRoute: ProjectRefAuthRouteWithChildren,
ProjectRefBranchesRoute: ProjectRefBranchesRouteWithChildren,
ProjectRefComputeRoute: ProjectRefComputeRouteWithChildren,
ProjectRefDatabaseRoute: ProjectRefDatabaseRouteWithChildren,
ProjectRefEditorRoute: ProjectRefEditorRouteWithChildren,
ProjectRefExplorerRoute: ProjectRefExplorerRouteWithChildren,
@@ -7060,7 +7061,6 @@ const ProjectRefRouteChildren: ProjectRefRouteChildren = {
ProjectRefSettingsRoute: ProjectRefSettingsRouteWithChildren,
ProjectRefSqlRoute: ProjectRefSqlRouteWithChildren,
ProjectRefStorageRoute: ProjectRefStorageRouteWithChildren,
ProjectRefWorkersRoute: ProjectRefWorkersRouteWithChildren,
ProjectRefIndexRoute: ProjectRefIndexRoute,
ProjectRefApiIndexRoute: ProjectRefApiIndexRoute,
}
@@ -0,0 +1,25 @@
import { createFileRoute, Outlet, useMatches } from '@tanstack/react-router'
import { ComputeLayout } from '@/components/layouts/ComputeLayout/ComputeLayout'
export const Route = createFileRoute('/project/$ref/compute')({
component: ComputeShell,
})
type ComputeStaticData = {
computeLayoutTitle?: string
}
function ComputeShell() {
const title = useMatches({
select: (matches) =>
(matches[matches.length - 1]?.staticData as ComputeStaticData | undefined)
?.computeLayoutTitle ?? '',
})
return (
<ComputeLayout title={title}>
<Outlet />
</ComputeLayout>
)
}
@@ -0,0 +1,14 @@
import { createFileRoute } from '@tanstack/react-router'
import ComputeInstanceDetailPage from '@/pages/project/[ref]/compute/[name]'
export const Route = createFileRoute('/project/$ref/compute/$name')({
component: ComputeInstanceDetailRoute,
staticData: {
computeLayoutTitle: 'Instance',
},
})
function ComputeInstanceDetailRoute() {
return <ComputeInstanceDetailPage dehydratedState={undefined} />
}
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { PRODUCT_NAME } from '@/lib/constants/compute'
import ComputePage from '@/pages/project/[ref]/compute/index'
export const Route = createFileRoute('/project/$ref/compute/')({
component: ComputeIndexRoute,
staticData: {
computeLayoutTitle: PRODUCT_NAME,
},
})
function ComputeIndexRoute() {
return <ComputePage dehydratedState={undefined} />
}
@@ -0,0 +1,20 @@
import { createFileRoute } from '@tanstack/react-router'
import ComputeSecretsPage, {
ComputeSecretsPageWrapper,
} from '@/pages/project/[ref]/compute/secrets'
export const Route = createFileRoute('/project/$ref/compute/secrets')({
component: ComputeSecretsRoute,
staticData: {
computeLayoutTitle: 'Secrets',
},
})
function ComputeSecretsRoute() {
return (
<ComputeSecretsPageWrapper>
<ComputeSecretsPage dehydratedState={undefined} />
</ComputeSecretsPageWrapper>
)
}
@@ -1,25 +0,0 @@
import { createFileRoute, Outlet, useMatches } from '@tanstack/react-router'
import { WorkersLayout } from '@/components/layouts/WorkersLayout/WorkersLayout'
export const Route = createFileRoute('/project/$ref/workers')({
component: WorkersShell,
})
type WorkersStaticData = {
workersLayoutTitle?: string
}
function WorkersShell() {
const title = useMatches({
select: (matches) =>
(matches[matches.length - 1]?.staticData as WorkersStaticData | undefined)
?.workersLayoutTitle ?? '',
})
return (
<WorkersLayout title={title}>
<Outlet />
</WorkersLayout>
)
}
@@ -1,14 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import WorkerDetailPage from '@/pages/project/[ref]/workers/[name]'
export const Route = createFileRoute('/project/$ref/workers/$name')({
component: WorkerDetailRoute,
staticData: {
workersLayoutTitle: 'Worker',
},
})
function WorkerDetailRoute() {
return <WorkerDetailPage dehydratedState={undefined} />
}
@@ -1,15 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import WorkersPage from '@/pages/project/[ref]/workers/index'
export const Route = createFileRoute('/project/$ref/workers/')({
component: WorkersIndexRoute,
staticData: {
workersLayoutTitle: PRODUCT_NAME,
},
})
function WorkersIndexRoute() {
return <WorkersPage dehydratedState={undefined} />
}
@@ -1,18 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import WorkerSecretsPage, { WorkerSecretsPageWrapper } from '@/pages/project/[ref]/workers/secrets'
export const Route = createFileRoute('/project/$ref/workers/secrets')({
component: WorkerSecretsRoute,
staticData: {
workersLayoutTitle: 'Secrets',
},
})
function WorkerSecretsRoute() {
return (
<WorkerSecretsPageWrapper>
<WorkerSecretsPage dehydratedState={undefined} />
</WorkerSecretsPageWrapper>
)
}
@@ -4,9 +4,9 @@ import type { components } from 'api-types'
import { HttpResponse } from 'msw'
import { beforeEach, describe, expect, it } from 'vitest'
import { workersQueryOptions } from '@/data/workers/workers-query'
import { PRODUCT_NAME } from '@/lib/constants/workers'
import WorkersPage from '@/pages/project/[ref]/workers/index'
import { computeQueryOptions } from '@/data/compute/compute-query'
import { PRODUCT_NAME } from '@/lib/constants/compute'
import ComputePage from '@/pages/project/[ref]/compute/index'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
import { routerMock } from '@/tests/lib/route-mock'
@@ -18,7 +18,7 @@ type WorkerDatum = ListWorkersResponse['data'][number]
// reads it no matter what the router URL says.
const PROJECT_REF = 'default'
const workerDatum = (
const computeInstanceDatum = (
id: string,
attributes: Partial<WorkerDatum['attributes']> = {}
): WorkerDatum => ({
@@ -32,47 +32,47 @@ const workerDatum = (
},
})
const mockWorkersList = (workers: WorkerDatum[]) =>
addAPIMock({ method: 'get', path: '/v2/projects/:ref/workers', response: { data: workers } })
const mockComputeInstancesList = (instances: WorkerDatum[]) =>
addAPIMock({ method: 'get', path: '/v2/projects/:ref/workers', response: { data: instances } })
const mockWorkersListFailure = (status: number) =>
const mockComputeInstancesListFailure = (status: number) =>
addAPIMock({
method: 'get',
path: '/v2/projects/:ref/workers',
response: () => HttpResponse.json<APIErrorBody>({ message: 'Denied' }, { status }),
})
// The workers list is platform-only, and `IS_PLATFORM` is false under vitest, so the page's own
// The compute list is platform-only, and `IS_PLATFORM` is false under vitest, so the page's own
// query never leaves the idle state. Filling the cache imperatively still goes through MSW, and
// the page reads whatever landed there.
const renderWorkersPage = async () => {
const renderComputePage = async () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
await queryClient
.fetchQuery(workersQueryOptions({ projectRef: PROJECT_REF }))
.fetchQuery(computeQueryOptions({ projectRef: PROJECT_REF }))
.catch(() => undefined)
return customRender(<WorkersPage dehydratedState={undefined} />, { queryClient })
return customRender(<ComputePage dehydratedState={undefined} />, { queryClient })
}
describe('/project/[ref]/workers', () => {
describe('/project/[ref]/compute', () => {
beforeEach(() => {
routerMock.setCurrentUrl(`/project/${PROJECT_REF}/workers`)
routerMock.setCurrentUrl(`/project/${PROJECT_REF}/compute`)
})
it('lists the workers the API returns', async () => {
mockWorkersList([
workerDatum('embed'),
workerDatum('resize', {
it('lists the instances the API returns', async () => {
mockComputeInstancesList([
computeInstanceDatum('embed'),
computeInstanceDatum('resize', {
build_state: 'building',
spec: { exposure: 'private', instances: 2, runtime: 'python', size: '4gb-2vcpu' },
}),
])
await renderWorkersPage()
await renderComputePage()
expect(screen.getByRole('link', { name: 'embed' })).toHaveAttribute(
'href',
`/project/${PROJECT_REF}/workers/embed`
`/project/${PROJECT_REF}/compute/embed`
)
expect(screen.getByRole('link', { name: 'resize' })).toBeVisible()
expect(screen.getByText('Active')).toBeVisible()
@@ -82,41 +82,41 @@ describe('/project/[ref]/workers', () => {
})
it('invites you to deploy one when the project has none', async () => {
mockWorkersList([])
mockComputeInstancesList([])
await renderWorkersPage()
await renderComputePage()
expect(screen.getByText('Deploy your first worker')).toBeVisible()
expect(screen.getByText('Deploy your first Compute instance')).toBeVisible()
expect(screen.queryByRole('table')).not.toBeInTheDocument()
})
it('refreshes the workers list on request', async () => {
mockWorkersList([workerDatum('existing')])
it('refreshes the instances list on request', async () => {
mockComputeInstancesList([computeInstanceDatum('existing')])
await renderWorkersPage()
await renderComputePage()
mockWorkersList([workerDatum('embed')])
mockComputeInstancesList([computeInstanceDatum('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)
mockComputeInstancesListFailure(404)
await renderWorkersPage()
await renderComputePage()
expect(screen.getByText(`${PRODUCT_NAME} is not enabled for this project`)).toBeVisible()
expect(screen.queryByRole('table')).not.toBeInTheDocument()
})
it('asks for permissions when the project is enrolled but the caller is not allowed', async () => {
mockWorkersListFailure(403)
mockComputeInstancesListFailure(403)
await renderWorkersPage()
await renderComputePage()
expect(
screen.getByText("You need additional permissions to view this project's workers")
screen.getByText("You need additional permissions to view this project's compute instances")
).toBeVisible()
expect(screen.queryByRole('table')).not.toBeInTheDocument()
})
@@ -131,11 +131,11 @@ describe('/project/[ref]/workers', () => {
return HttpResponse.json<APIErrorBody>({ message: 'Unavailable' }, { status: 500 })
}
return HttpResponse.json<ListWorkersResponse>({ data: [workerDatum('embed')] })
return HttpResponse.json<ListWorkersResponse>({ data: [computeInstanceDatum('embed')] })
},
})
await renderWorkersPage()
await renderComputePage()
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
+1 -1
View File
@@ -3681,7 +3681,7 @@ export interface UnifiedLogsRowClickedEvent {
| 'supavisor'
| 'pgbouncer'
| 'multigres'
| 'workers'
| 'compute'
}
groups: TelemetryGroups
}
+10 -10
View File
@@ -124,6 +124,16 @@ export const Index: Record<string, any> = [
svg: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M3 3V21M7.5 3V21M12 3V21M16.5 3V21M21 10V14\"/>\n</svg>\n",
jsx: "import { ClickHouse } from \"icons\"\n <ClickHouse/>\n "
},
{
name: "compute",
componentName: "Compute",
deprecated: false,
raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name Compute\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAyNCAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41Ij48cGF0aCBkPSJNNy41NTU1NiA4LjcwMDQ5TDEyIDExLjUwMTFNMTIgMTEuNTAxMUwxNi40NDQ0IDguNzAwNDlNMTIgMTEuNTAxMUwxMiAxNi40MDFNMjIgMTUuNzAwOVY3LjI5OTE0QzIxLjk5OTYgNi45MzA4IDIxLjg5NjcgNi41NjkwNCAyMS43MDE3IDYuMjUwMTRDMjEuNTA2NyA1LjkzMTI1IDIxLjIyNjQgNS42NjY0MyAyMC44ODg5IDUuNDgyMjdMMTMuMTExMSAxLjI4MTRDMTIuNzczMyAxLjA5NzA1IDEyLjM5MDEgMSAxMiAxQzExLjYwOTkgMSAxMS4yMjY3IDEuMDk3MDUgMTAuODg4OSAxLjI4MTRMMy4xMTExMSA1LjQ4MjI3QzIuNzczNjMgNS42NjY0MyAyLjQ5MzMxIDUuOTMxMjUgMi4yOTgyOSA2LjI1MDE0QzIuMTAzMjcgNi41NjkwNCAyLjAwMDQgNi45MzA4IDIgNy4yOTkxNFYxNS43MDA5QzIuMDAwNCAxNi4wNjkyIDIuMTAzMjcgMTYuNDMxIDIuMjk4MjkgMTYuNzQ5OUMyLjQ5MzMxIDE3LjA2ODggMi43NzM2MyAxNy4zMzM2IDMuMTExMTEgMTcuNTE3N0wxMC44ODg5IDIxLjcxODZDMTEuMjI2NyAyMS45MDI5IDExLjYwOTkgMjIgMTIgMjJDMTIuMzkwMSAyMiAxMi43NzMzIDIxLjkwMjkgMTMuMTExMSAyMS43MTg2TDIwLjg4ODkgMTcuNTE3N0MyMS4yMjY0IDE3LjMzMzYgMjEuNTA2NyAxNy4wNjg4IDIxLjcwMTcgMTYuNzQ5OUMyMS44OTY3IDE2LjQzMSAyMS45OTk2IDE2LjA2OTIgMjIgMTUuNzAwOVpNMTcuOTI1OSAxMy43NDJWOS4yNjAxNUMxNy45MjU3IDkuMDYzNjYgMTcuODY0NyA4Ljg3MDY4IDE3Ljc0OTIgOC43MDA1N0MxNy42MzM2IDguNTMwNDYgMTcuNDY3NSA4LjM4OTE5IDE3LjI2NzUgOC4yOTA5NUwxMi42NTg0IDYuMDUwMDNDMTIuNDU4MiA1Ljk1MTY5IDEyLjIzMTIgNS44OTk5MiAxMiA1Ljg5OTkyQzExLjc2ODggNS44OTk5MiAxMS41NDE4IDUuOTUxNjkgMTEuMzQxNiA2LjA1MDAzTDYuNzMyNTEgOC4yOTA5NUM2LjUzMjUyIDguMzg5MTkgNi4zNjY0MSA4LjUzMDQ2IDYuMjUwODQgOC43MDA1N0M2LjEzNTI3IDguODcwNjggNi4wNzQzMSA5LjA2MzY2IDYuMDc0MDcgOS4yNjAxNVYxMy43NDJDNi4wNzQzMSAxMy45Mzg1IDYuMTM1MjcgMTQuMTMxNSA2LjI1MDg0IDE0LjMwMTZDNi4zNjY0MSAxNC40NzE3IDYuNTMyNTIgMTQuNjEyOSA2LjczMjUxIDE0LjcxMTJMMTEuMzQxNiAxNi45NTIxQzExLjU0MTggMTcuMDUwNCAxMS43Njg4IDE3LjEwMjIgMTIgMTcuMTAyMkMxMi4yMzEyIDE3LjEwMjIgMTIuNDU4MiAxNy4wNTA0IDEyLjY1ODQgMTYuOTUyMUwxNy4yNjc1IDE0LjcxMTJDMTcuNDY3NSAxNC42MTI5IDE3LjYzMzYgMTQuNDcxNyAxNy43NDkyIDE0LjMwMTZDMTcuODY0NyAxNC4xMzE1IDE3LjkyNTcgMTMuOTM4NSAxNy45MjU5IDEzLjc0MloiLz4KPC9zdmc+Cg==)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst Compute = createSupabaseIcon(\n 'Compute',\n [\n [\n 'path',\n {\n d: 'M7.55556 8.70049L12 11.5011M12 11.5011L16.4444 8.70049M12 11.5011L12 16.401M22 15.7009V7.29914C21.9996 6.9308 21.8967 6.56904 21.7017 6.25014C21.5067 5.93125 21.2264 5.66643 20.8889 5.48227L13.1111 1.2814C12.7733 1.09705 12.3901 1 12 1C11.6099 1 11.2267 1.09705 10.8889 1.2814L3.11111 5.48227C2.77363 5.66643 2.49331 5.93125 2.29829 6.25014C2.10327 6.56904 2.0004 6.9308 2 7.29914V15.7009C2.0004 16.0692 2.10327 16.431 2.29829 16.7499C2.49331 17.0688 2.77363 17.3336 3.11111 17.5177L10.8889 21.7186C11.2267 21.9029 11.6099 22 12 22C12.3901 22 12.7733 21.9029 13.1111 21.7186L20.8889 17.5177C21.2264 17.3336 21.5067 17.0688 21.7017 16.7499C21.8967 16.431 21.9996 16.0692 22 15.7009ZM17.9259 13.742V9.26015C17.9257 9.06366 17.8647 8.87068 17.7492 8.70057C17.6336 8.53046 17.4675 8.38919 17.2675 8.29095L12.6584 6.05003C12.4582 5.95169 12.2312 5.89992 12 5.89992C11.7688 5.89992 11.5418 5.95169 11.3416 6.05003L6.73251 8.29095C6.53252 8.38919 6.36641 8.53046 6.25084 8.70057C6.13527 8.87068 6.07431 9.06366 6.07407 9.26015V13.742C6.07431 13.9385 6.13527 14.1315 6.25084 14.3016C6.36641 14.4717 6.53252 14.6129 6.73251 14.7112L11.3416 16.9521C11.5418 17.0504 11.7688 17.1022 12 17.1022C12.2312 17.1022 12.4582 17.0504 12.6584 16.9521L17.2675 14.7112C17.4675 14.6129 17.6336 14.4717 17.7492 14.3016C17.8647 14.1315 17.9257 13.9385 17.9259 13.742Z',\n key: '1cro8t',\n },\n ],\n ],\n { fill: 'none', stroke: 'currentColor', strokeWidth: '1.5' },\n);\n\nexport default Compute;\n",
component: React.lazy(() => import('icons/src/icons/compute')),
import: "import { Compute } from 'icons'",
svg: "<svg width=\"24\" height=\"23\" viewBox=\"0 0 24 23\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" stroke=\"currentColor\" stroke-width=\"1.5\">\n<path d=\"M7.55556 8.70049L12 11.5011M12 11.5011L16.4444 8.70049M12 11.5011L12 16.401M22 15.7009V7.29914C21.9996 6.9308 21.8967 6.56904 21.7017 6.25014C21.5067 5.93125 21.2264 5.66643 20.8889 5.48227L13.1111 1.2814C12.7733 1.09705 12.3901 1 12 1C11.6099 1 11.2267 1.09705 10.8889 1.2814L3.11111 5.48227C2.77363 5.66643 2.49331 5.93125 2.29829 6.25014C2.10327 6.56904 2.0004 6.9308 2 7.29914V15.7009C2.0004 16.0692 2.10327 16.431 2.29829 16.7499C2.49331 17.0688 2.77363 17.3336 3.11111 17.5177L10.8889 21.7186C11.2267 21.9029 11.6099 22 12 22C12.3901 22 12.7733 21.9029 13.1111 21.7186L20.8889 17.5177C21.2264 17.3336 21.5067 17.0688 21.7017 16.7499C21.8967 16.431 21.9996 16.0692 22 15.7009ZM17.9259 13.742V9.26015C17.9257 9.06366 17.8647 8.87068 17.7492 8.70057C17.6336 8.53046 17.4675 8.38919 17.2675 8.29095L12.6584 6.05003C12.4582 5.95169 12.2312 5.89992 12 5.89992C11.7688 5.89992 11.5418 5.95169 11.3416 6.05003L6.73251 8.29095C6.53252 8.38919 6.36641 8.53046 6.25084 8.70057C6.13527 8.87068 6.07431 9.06366 6.07407 9.26015V13.742C6.07431 13.9385 6.13527 14.1315 6.25084 14.3016C6.36641 14.4717 6.53252 14.6129 6.73251 14.7112L11.3416 16.9521C11.5418 17.0504 11.7688 17.1022 12 17.1022C12.2312 17.1022 12.4582 17.0504 12.6584 16.9521L17.2675 14.7112C17.4675 14.6129 17.6336 14.4717 17.7492 14.3016C17.8647 14.1315 17.9257 13.9385 17.9259 13.742Z\"/>\n</svg>\n",
jsx: "import { Compute } from \"icons\"\n <Compute/>\n "
},
{
name: "database",
componentName: "Database",
@@ -363,15 +373,5 @@ export const Index: Record<string, any> = [
import: "import { VectorBucket } from 'icons'",
svg: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M19.9999 20C20.5303 20 21.039 19.7893 21.4141 19.4142C21.7892 19.0391 21.9999 18.5304 21.9999 18V8C21.9999 7.46957 21.7892 6.96086 21.4141 6.58579C21.039 6.21071 20.5303 6 19.9999 6H12.0999C11.7654 6.00328 11.4354 5.92261 11.1402 5.76538C10.8449 5.60815 10.5938 5.37938 10.4099 5.1L9.59988 3.9C9.41777 3.62347 9.16985 3.39648 8.87838 3.2394C8.5869 3.08231 8.26099 3.00005 7.92988 3H3.99988C3.46944 3 2.96074 3.21071 2.58566 3.58579C2.21059 3.96086 1.99988 4.46957 1.99988 5V18C1.99988 18.5304 2.21059 19.0391 2.58566 19.4142C2.96074 19.7893 3.46944 20 3.99988 20H19.9999Z\"/>\n<path d=\"M11.7833 15.2197V19.5M11.7833 15.2197L16.8327 12.2868M11.7833 15.2197L6.73425 12.2869M6.73425 12.2869V15.7965M6.73425 12.2869V12.2568L9.75885 10.5M16.8328 15.8272V12.2568L13.8083 10.5\"/>\n</svg>\n",
jsx: "import { VectorBucket } from \"icons\"\n <VectorBucket/>\n "
},
{
name: "workers",
componentName: "Workers",
deprecated: false,
raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name Workers\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAyNCAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41Ij48cGF0aCBkPSJNNy41NTU1NiA4LjcwMDQ5TDEyIDExLjUwMTFNMTIgMTEuNTAxMUwxNi40NDQ0IDguNzAwNDlNMTIgMTEuNTAxMUwxMiAxNi40MDFNMjIgMTUuNzAwOVY3LjI5OTE0QzIxLjk5OTYgNi45MzA4IDIxLjg5NjcgNi41NjkwNCAyMS43MDE3IDYuMjUwMTRDMjEuNTA2NyA1LjkzMTI1IDIxLjIyNjQgNS42NjY0MyAyMC44ODg5IDUuNDgyMjdMMTMuMTExMSAxLjI4MTRDMTIuNzczMyAxLjA5NzA1IDEyLjM5MDEgMSAxMiAxQzExLjYwOTkgMSAxMS4yMjY3IDEuMDk3MDUgMTAuODg4OSAxLjI4MTRMMy4xMTExMSA1LjQ4MjI3QzIuNzczNjMgNS42NjY0MyAyLjQ5MzMxIDUuOTMxMjUgMi4yOTgyOSA2LjI1MDE0QzIuMTAzMjcgNi41NjkwNCAyLjAwMDQgNi45MzA4IDIgNy4yOTkxNFYxNS43MDA5QzIuMDAwNCAxNi4wNjkyIDIuMTAzMjcgMTYuNDMxIDIuMjk4MjkgMTYuNzQ5OUMyLjQ5MzMxIDE3LjA2ODggMi43NzM2MyAxNy4zMzM2IDMuMTExMTEgMTcuNTE3N0wxMC44ODg5IDIxLjcxODZDMTEuMjI2NyAyMS45MDI5IDExLjYwOTkgMjIgMTIgMjJDMTIuMzkwMSAyMiAxMi43NzMzIDIxLjkwMjkgMTMuMTExMSAyMS43MTg2TDIwLjg4ODkgMTcuNTE3N0MyMS4yMjY0IDE3LjMzMzYgMjEuNTA2NyAxNy4wNjg4IDIxLjcwMTcgMTYuNzQ5OUMyMS44OTY3IDE2LjQzMSAyMS45OTk2IDE2LjA2OTIgMjIgMTUuNzAwOVpNMTcuOTI1OSAxMy43NDJWOS4yNjAxNUMxNy45MjU3IDkuMDYzNjYgMTcuODY0NyA4Ljg3MDY4IDE3Ljc0OTIgOC43MDA1N0MxNy42MzM2IDguNTMwNDYgMTcuNDY3NSA4LjM4OTE5IDE3LjI2NzUgOC4yOTA5NUwxMi42NTg0IDYuMDUwMDNDMTIuNDU4MiA1Ljk1MTY5IDEyLjIzMTIgNS44OTk5MiAxMiA1Ljg5OTkyQzExLjc2ODggNS44OTk5MiAxMS41NDE4IDUuOTUxNjkgMTEuMzQxNiA2LjA1MDAzTDYuNzMyNTEgOC4yOTA5NUM2LjUzMjUyIDguMzg5MTkgNi4zNjY0MSA4LjUzMDQ2IDYuMjUwODQgOC43MDA1N0M2LjEzNTI3IDguODcwNjggNi4wNzQzMSA5LjA2MzY2IDYuMDc0MDcgOS4yNjAxNVYxMy43NDJDNi4wNzQzMSAxMy45Mzg1IDYuMTM1MjcgMTQuMTMxNSA2LjI1MDg0IDE0LjMwMTZDNi4zNjY0MSAxNC40NzE3IDYuNTMyNTIgMTQuNjEyOSA2LjczMjUxIDE0LjcxMTJMMTEuMzQxNiAxNi45NTIxQzExLjU0MTggMTcuMDUwNCAxMS43Njg4IDE3LjEwMjIgMTIgMTcuMTAyMkMxMi4yMzEyIDE3LjEwMjIgMTIuNDU4MiAxNy4wNTA0IDEyLjY1ODQgMTYuOTUyMUwxNy4yNjc1IDE0LjcxMTJDMTcuNDY3NSAxNC42MTI5IDE3LjYzMzYgMTQuNDcxNyAxNy43NDkyIDE0LjMwMTZDMTcuODY0NyAxNC4xMzE1IDE3LjkyNTcgMTMuOTM4NSAxNy45MjU5IDEzLjc0MloiLz4KPC9zdmc+Cg==)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst Workers = createSupabaseIcon(\n 'Workers',\n [\n [\n 'path',\n {\n d: 'M7.55556 8.70049L12 11.5011M12 11.5011L16.4444 8.70049M12 11.5011L12 16.401M22 15.7009V7.29914C21.9996 6.9308 21.8967 6.56904 21.7017 6.25014C21.5067 5.93125 21.2264 5.66643 20.8889 5.48227L13.1111 1.2814C12.7733 1.09705 12.3901 1 12 1C11.6099 1 11.2267 1.09705 10.8889 1.2814L3.11111 5.48227C2.77363 5.66643 2.49331 5.93125 2.29829 6.25014C2.10327 6.56904 2.0004 6.9308 2 7.29914V15.7009C2.0004 16.0692 2.10327 16.431 2.29829 16.7499C2.49331 17.0688 2.77363 17.3336 3.11111 17.5177L10.8889 21.7186C11.2267 21.9029 11.6099 22 12 22C12.3901 22 12.7733 21.9029 13.1111 21.7186L20.8889 17.5177C21.2264 17.3336 21.5067 17.0688 21.7017 16.7499C21.8967 16.431 21.9996 16.0692 22 15.7009ZM17.9259 13.742V9.26015C17.9257 9.06366 17.8647 8.87068 17.7492 8.70057C17.6336 8.53046 17.4675 8.38919 17.2675 8.29095L12.6584 6.05003C12.4582 5.95169 12.2312 5.89992 12 5.89992C11.7688 5.89992 11.5418 5.95169 11.3416 6.05003L6.73251 8.29095C6.53252 8.38919 6.36641 8.53046 6.25084 8.70057C6.13527 8.87068 6.07431 9.06366 6.07407 9.26015V13.742C6.07431 13.9385 6.13527 14.1315 6.25084 14.3016C6.36641 14.4717 6.53252 14.6129 6.73251 14.7112L11.3416 16.9521C11.5418 17.0504 11.7688 17.1022 12 17.1022C12.2312 17.1022 12.4582 17.0504 12.6584 16.9521L17.2675 14.7112C17.4675 14.6129 17.6336 14.4717 17.7492 14.3016C17.8647 14.1315 17.9257 13.9385 17.9259 13.742Z',\n key: '1cro8t',\n },\n ],\n ],\n { fill: 'none', stroke: 'currentColor', strokeWidth: '1.5' },\n);\n\nexport default Workers;\n",
component: React.lazy(() => import('icons/src/icons/workers')),
import: "import { Workers } from 'icons'",
svg: "<svg width=\"24\" height=\"23\" viewBox=\"0 0 24 23\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" stroke=\"currentColor\" stroke-width=\"1.5\">\n<path d=\"M7.55556 8.70049L12 11.5011M12 11.5011L16.4444 8.70049M12 11.5011L12 16.401M22 15.7009V7.29914C21.9996 6.9308 21.8967 6.56904 21.7017 6.25014C21.5067 5.93125 21.2264 5.66643 20.8889 5.48227L13.1111 1.2814C12.7733 1.09705 12.3901 1 12 1C11.6099 1 11.2267 1.09705 10.8889 1.2814L3.11111 5.48227C2.77363 5.66643 2.49331 5.93125 2.29829 6.25014C2.10327 6.56904 2.0004 6.9308 2 7.29914V15.7009C2.0004 16.0692 2.10327 16.431 2.29829 16.7499C2.49331 17.0688 2.77363 17.3336 3.11111 17.5177L10.8889 21.7186C11.2267 21.9029 11.6099 22 12 22C12.3901 22 12.7733 21.9029 13.1111 21.7186L20.8889 17.5177C21.2264 17.3336 21.5067 17.0688 21.7017 16.7499C21.8967 16.431 21.9996 16.0692 22 15.7009ZM17.9259 13.742V9.26015C17.9257 9.06366 17.8647 8.87068 17.7492 8.70057C17.6336 8.53046 17.4675 8.38919 17.2675 8.29095L12.6584 6.05003C12.4582 5.95169 12.2312 5.89992 12 5.89992C11.7688 5.89992 11.5418 5.95169 11.3416 6.05003L6.73251 8.29095C6.53252 8.38919 6.36641 8.53046 6.25084 8.70057C6.13527 8.87068 6.07431 9.06366 6.07407 9.26015V13.742C6.07431 13.9385 6.13527 14.1315 6.25084 14.3016C6.36641 14.4717 6.53252 14.6129 6.73251 14.7112L11.3416 16.9521C11.5418 17.0504 11.7688 17.1022 12 17.1022C12.2312 17.1022 12.4582 17.0504 12.6584 16.9521L17.2675 14.7112C17.4675 14.6129 17.6336 14.4717 17.7492 14.3016C17.8647 14.1315 17.9257 13.9385 17.9259 13.742Z\"/>\n</svg>\n",
jsx: "import { Workers } from \"icons\"\n <Workers/>\n "
}
]
@@ -1,7 +1,7 @@
import createSupabaseIcon from '../createSupabaseIcon';
/**
* @component @name Workers
* @component @name Compute
* @description Supabase SVG icon component, renders SVG Element with children.
*
* @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAyNCAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41Ij48cGF0aCBkPSJNNy41NTU1NiA4LjcwMDQ5TDEyIDExLjUwMTFNMTIgMTEuNTAxMUwxNi40NDQ0IDguNzAwNDlNMTIgMTEuNTAxMUwxMiAxNi40MDFNMjIgMTUuNzAwOVY3LjI5OTE0QzIxLjk5OTYgNi45MzA4IDIxLjg5NjcgNi41NjkwNCAyMS43MDE3IDYuMjUwMTRDMjEuNTA2NyA1LjkzMTI1IDIxLjIyNjQgNS42NjY0MyAyMC44ODg5IDUuNDgyMjdMMTMuMTExMSAxLjI4MTRDMTIuNzczMyAxLjA5NzA1IDEyLjM5MDEgMSAxMiAxQzExLjYwOTkgMSAxMS4yMjY3IDEuMDk3MDUgMTAuODg4OSAxLjI4MTRMMy4xMTExMSA1LjQ4MjI3QzIuNzczNjMgNS42NjY0MyAyLjQ5MzMxIDUuOTMxMjUgMi4yOTgyOSA2LjI1MDE0QzIuMTAzMjcgNi41NjkwNCAyLjAwMDQgNi45MzA4IDIgNy4yOTkxNFYxNS43MDA5QzIuMDAwNCAxNi4wNjkyIDIuMTAzMjcgMTYuNDMxIDIuMjk4MjkgMTYuNzQ5OUMyLjQ5MzMxIDE3LjA2ODggMi43NzM2MyAxNy4zMzM2IDMuMTExMTEgMTcuNTE3N0wxMC44ODg5IDIxLjcxODZDMTEuMjI2NyAyMS45MDI5IDExLjYwOTkgMjIgMTIgMjJDMTIuMzkwMSAyMiAxMi43NzMzIDIxLjkwMjkgMTMuMTExMSAyMS43MTg2TDIwLjg4ODkgMTcuNTE3N0MyMS4yMjY0IDE3LjMzMzYgMjEuNTA2NyAxNy4wNjg4IDIxLjcwMTcgMTYuNzQ5OUMyMS44OTY3IDE2LjQzMSAyMS45OTk2IDE2LjA2OTIgMjIgMTUuNzAwOVpNMTcuOTI1OSAxMy43NDJWOS4yNjAxNUMxNy45MjU3IDkuMDYzNjYgMTcuODY0NyA4Ljg3MDY4IDE3Ljc0OTIgOC43MDA1N0MxNy42MzM2IDguNTMwNDYgMTcuNDY3NSA4LjM4OTE5IDE3LjI2NzUgOC4yOTA5NUwxMi42NTg0IDYuMDUwMDNDMTIuNDU4MiA1Ljk1MTY5IDEyLjIzMTIgNS44OTk5MiAxMiA1Ljg5OTkyQzExLjc2ODggNS44OTk5MiAxMS41NDE4IDUuOTUxNjkgMTEuMzQxNiA2LjA1MDAzTDYuNzMyNTEgOC4yOTA5NUM2LjUzMjUyIDguMzg5MTkgNi4zNjY0MSA4LjUzMDQ2IDYuMjUwODQgOC43MDA1N0M2LjEzNTI3IDguODcwNjggNi4wNzQzMSA5LjA2MzY2IDYuMDc0MDcgOS4yNjAxNVYxMy43NDJDNi4wNzQzMSAxMy45Mzg1IDYuMTM1MjcgMTQuMTMxNSA2LjI1MDg0IDE0LjMwMTZDNi4zNjY0MSAxNC40NzE3IDYuNTMyNTIgMTQuNjEyOSA2LjczMjUxIDE0LjcxMTJMMTEuMzQxNiAxNi45NTIxQzExLjU0MTggMTcuMDUwNCAxMS43Njg4IDE3LjEwMjIgMTIgMTcuMTAyMkMxMi4yMzEyIDE3LjEwMjIgMTIuNDU4MiAxNy4wNTA0IDEyLjY1ODQgMTYuOTUyMUwxNy4yNjc1IDE0LjcxMTJDMTcuNDY3NSAxNC42MTI5IDE3LjYzMzYgMTQuNDcxNyAxNy43NDkyIDE0LjMwMTZDMTcuODY0NyAxNC4xMzE1IDE3LjkyNTcgMTMuOTM4NSAxNy45MjU5IDEzLjc0MloiLz4KPC9zdmc+Cg==)
@@ -10,8 +10,8 @@ import createSupabaseIcon from '../createSupabaseIcon';
* @returns {JSX.Element} JSX Element
*
*/
const Workers = createSupabaseIcon(
'Workers',
const Compute = createSupabaseIcon(
'Compute',
[
[
'path',
@@ -24,4 +24,4 @@ const Workers = createSupabaseIcon(
{ fill: 'none', stroke: 'currentColor', strokeWidth: '1.5' },
);
export default Workers;
export default Compute;
+1 -1
View File
@@ -10,6 +10,7 @@ export { default as BucketPlus } from './bucket-plus';
export { default as Chatgpt } from './chatgpt';
export { default as Claude } from './claude';
export { default as ClickHouse } from './click-house';
export { default as Compute } from './compute';
export { default as Database } from './database';
export { default as Datadog } from './datadog';
export { default as EdgeFunctions } from './edge-functions';
@@ -34,5 +35,4 @@ export { default as Storage } from './storage';
export { default as TableEditor } from './table-editor';
export { default as User } from './user';
export { default as VectorBucket } from './vector-bucket';
export { default as Workers } from './workers';