From 06f4f15c61ddb04bcbc4f4b8fe35eedfed42d42e Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 18 Aug 2026 14:49:42 +0200 Subject: [PATCH] feat(studio): read the worker detail from the v2 Management API Overview now shows the instance tally, build failure reason and instance errors the endpoint returns, replacing the seeded request and resource charts. --- .../Workers/WorkerDetail/WorkerDetail.tsx | 19 +- .../WorkerDetail/WorkerOverviewTab.tsx | 416 +++--------------- .../WorkerDetail/WorkerSettingsTab.tsx | 44 +- .../Workers/WorkerDetail/workerCharts.ts | 156 ------- 4 files changed, 106 insertions(+), 529 deletions(-) delete mode 100644 apps/studio/components/interfaces/Workers/WorkerDetail/workerCharts.ts diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx index 6cb03dd668f..2e7c777cc11 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { useParams } from 'common' -import dayjs from 'dayjs' -import { Clock, Container, Copy, Info } from 'lucide-react' +import { Container, Copy, Info, Package } from 'lucide-react' import Link from 'next/link' import { parseAsStringEnum, useQueryState } from 'nuqs' import { @@ -118,12 +117,14 @@ export const WorkerDetail = () => { {worker.name} - + - - - Last deployed {dayjs(worker.updatedAt).format('MMM D, YYYY HH:mm')} - + {worker.imageVersion !== undefined && ( + + + Image {worker.imageVersion} + + )} @@ -140,7 +141,7 @@ export const WorkerDetail = () => { runtime: worker.runtime, size: worker.size, access: worker.access, - instances: worker.instances, + instances: worker.declaredInstances, }} tabs={['curl', 'js', 'python']} /> @@ -175,7 +176,7 @@ export const WorkerDetail = () => { - {tab === 'overview' && } + {tab === 'overview' && } {tab === 'logs' && ( diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx index 66793366c0a..63802bfe2d5 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx @@ -1,22 +1,7 @@ -import { BarChart2, Check, ExternalLink } from 'lucide-react' -import { useMemo, useState } from 'react' -import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' -import { - Chart, - ChartCard, - ChartContent, - ChartEmptyState, - ChartHeader, - ChartLine, - ChartLoadingState, - ChartMetric, -} from 'ui-patterns/Chart' -import { LogsBarChart } from 'ui-patterns/LogsBarChart' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, - PageSectionAside, PageSectionContent, PageSectionMeta, PageSectionSummary, @@ -24,345 +9,84 @@ import { } from 'ui-patterns/PageSection' import { WorkerCommandLine } from '../WorkerCommandLine' -import { WORKER_ERROR_REASON_LABEL } from '../Workers.constants' import type { Worker } from '../Workers.types' -import { - buildWorkerChartData, - CHART_INTERVALS, - CPU_TIME_CHART_CONFIG, - formatMetric, - formatRate, - getSegmentedButtonClassName, - meanBy, - MEMORY_CHART_CONFIG, - RESPONSE_TIME_CHART_CONFIG, - sumBy, -} from './workerCharts' import { CLI_NAME } from '@/lib/constants/workers' interface WorkerOverviewTabProps { - projectRef: string worker: Worker } -const WorkerChartEmpty = () => ( - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> +const InstanceCount = ({ label, value }: { label: string; value: number }) => ( +
+ {label} + {value} +
) -export const WorkerOverviewTab = ({ projectRef, worker }: WorkerOverviewTabProps) => { - const [interval, setInterval] = useState('1hr') - const selectedInterval = - CHART_INTERVALS.find((item) => item.key === interval) ?? CHART_INTERVALS[1] - const dateTimeFormat = selectedInterval.format - - const { invocation, metrics } = useMemo( - () => buildWorkerChartData(worker, interval), - [worker, interval] - ) - - const { totalRequests, totalErrors, totalWarnings } = useMemo( - () => ({ - totalRequests: sumBy(invocation, (d) => d.ok_count + d.warning_count + d.error_count), - totalErrors: sumBy(invocation, (d) => d.error_count), - totalWarnings: sumBy(invocation, (d) => d.warning_count), - }), - [invocation] - ) - - const { averageResponseTime, maxResponseTime } = useMemo( - () => ({ - averageResponseTime: meanBy(metrics, (d) => d.avg_response_time), - maxResponseTime: - metrics.length === 0 ? 0 : Math.max(...metrics.map((d) => d.max_response_time)), - }), - [metrics] - ) - - const { averageCpuTime, maxCpuTime, averageMemory, peakMemoryPercent } = useMemo( - () => ({ - averageCpuTime: meanBy(metrics, (d) => d.avg_cpu_time_used), - maxCpuTime: metrics.length === 0 ? 0 : Math.max(...metrics.map((d) => d.max_cpu_time_used)), - averageMemory: meanBy(metrics, (d) => d.avg_memory_used), - peakMemoryPercent: - metrics.length === 0 ? 0 : Math.max(...metrics.map((d) => d.memory_percent)), - }), - [metrics] - ) - - const isErrored = worker.state === 'errored' - const hasNoData = invocation.length === 0 - - return ( - <> - - -
- - -
- - - -
-
- -
- {CHART_INTERVALS.map((item, index) => ( - - ))} -
-
-
- -
- } - /> -
-
-
-
-
- - - - - - Errors in the last 24h - - - - - - - {isErrored ? ( - -

- {worker.errorReason - ? WORKER_ERROR_REASON_LABEL[worker.errorReason] - : 'The worker stopped unexpectedly.'} -

- - - } +export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => ( + + {worker.buildState === 'failed' && ( + + + +
+

{worker.stateReason ?? 'The build did not complete.'}

+ - ) : ( -
-
- -
- There have been{' '} - - {totalRequests.toLocaleString('en-US')} requests - {' '} - since last deploy and no errors. -
-
-
- )} - - - - - - - Performance - - - - - - -
- - -
-
- } - loadingState={} - > -
- `${Math.round(value)}ms`, - }} - /> -
-
-
-
-
-
- - - - - Usage - - - -
- - - -
- - -
-
- } - loadingState={} - > -
- `${Math.round(value)}ms`, - }} - /> -
-
-
-
- - - - -
- - -
-
- } - loadingState={} - > -
- `${Number(value).toFixed(1)}MB`, - }} - /> -
-
-
-
-
-
- - - ) -} + + + + )} + + {worker.instancesError !== undefined && ( + + + + {worker.instancesError} + + + + )} + + + + + Instances + + + + {worker.instances === undefined ? ( +

+ No instances are running for this worker yet. +

+ ) : ( +
+ + + + +
+ )} +
+
+ + + + + Logs and metrics + + + +

+ Request metrics are not exposed by the Management API yet. Stream logs from the CLI. +

+
+ +
+
+
+ +) diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerSettingsTab.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerSettingsTab.tsx index c4d9564090e..ba38ff51952 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerSettingsTab.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerSettingsTab.tsx @@ -13,13 +13,9 @@ import { import { RuntimeBadge } from '../RuntimeBadge' import { WorkerCommandLine } from '../WorkerCommandLine' -import { - getRuntimeMeta, - getSizeMeta, - LISTENING_PORT, - WORKERS_REGION_LABEL, -} from '../Workers.constants' +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 { WorkerSnippetTabs } from '../WorkerSnippetTabs' import CopyButton from '@/components/ui/CopyButton' @@ -50,7 +46,6 @@ const SettingsRow = ({ export const WorkerSettingsTab = ({ worker }: WorkerSettingsTabProps) => { const runtime = getRuntimeMeta(worker.runtime) - const size = getSizeMeta(worker.size) const commands = buildWorkerCliCommands(worker.name) const snippetInput = { @@ -58,7 +53,7 @@ export const WorkerSettingsTab = ({ worker }: WorkerSettingsTabProps) => { runtime: worker.runtime, size: worker.size, access: worker.access, - instances: worker.instances, + instances: worker.declaredInstances, } return ( @@ -78,14 +73,27 @@ export const WorkerSettingsTab = ({ worker }: WorkerSettingsTabProps) => { - - {runtime.baseImage} - - - - {runtime.entrypoint} - - + {worker.imageVersion !== undefined && ( + + + {worker.imageVersion} + + + )} + {runtime !== undefined && ( + + + {runtime.baseImage} + + + )} + {runtime !== undefined && ( + + + {runtime.entrypoint} + + + )} $PORT → {LISTENING_PORT} @@ -104,9 +112,9 @@ export const WorkerSettingsTab = ({ worker }: WorkerSettingsTabProps) => {
- {size.label} + {formatSize(worker.size)} - {worker.instances} + {worker.declaredInstances} {worker.access === 'public' ? ( Public diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/workerCharts.ts b/apps/studio/components/interfaces/Workers/WorkerDetail/workerCharts.ts deleted file mode 100644 index 2c3a344b774..00000000000 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/workerCharts.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { ChartConfig } from 'ui' - -import type { Worker } from '../Workers.types' - -// Seeded time-series until the workers metrics endpoint ships. - -export const CHART_INTERVALS = [ - { key: '15min', label: '15 min', format: 'MMM D, h:mm:ssa', minutes: 15 }, - { key: '1hr', label: '1 hour', format: 'MMM D, h:mma', minutes: 60 }, - { key: '3hr', label: '3 hours', format: 'MMM D, h:mma', minutes: 180 }, - { key: '1day', label: '1 day', format: 'MMM D, h:mma', minutes: 24 * 60 }, -] as const - -export type ChartIntervalKey = (typeof CHART_INTERVALS)[number]['key'] - -// Fixed base so SSR and client renders produce identical timestamps. -const CHART_END_ISO = '2026-08-10T09:00:00.000Z' - -export type WorkerInvocationDatum = { - timestamp: string - ok_count: number - warning_count: number - error_count: number -} - -export type WorkerMetricsDatum = { - timestamp: string - ok_count: number - warning_count: number - error_count: number - avg_response_time: number - max_response_time: number - avg_cpu_time_used: number - max_cpu_time_used: number - avg_memory_used: number - memory_percent: number -} - -const hashString = (input: string): number => { - let hash = 2166136261 - for (let i = 0; i < input.length; i++) { - hash ^= input.charCodeAt(i) - hash = Math.imul(hash, 16777619) - } - return (hash >>> 0) / 0xffffffff -} - -const pseudoNoise = (seed: number, amplitude = 1) => { - const value = Math.sin(seed * 12.9898) * 43758.5453 - return (value - Math.floor(value)) * amplitude -} - -export const getIntervalMinutes = (intervalKey: string) => - CHART_INTERVALS.find((item) => item.key === intervalKey)?.minutes ?? 60 - -// Not serving traffic, so the charts render empty rather than stale numbers. -export const isIdleState = (worker: Pick) => - worker.state === 'suspended' || - worker.state === 'killed' || - worker.state === 'deploying' || - worker.state === 'draining' - -export interface WorkerChartData { - invocation: WorkerInvocationDatum[] - metrics: WorkerMetricsDatum[] -} - -export const buildWorkerChartData = ( - worker: Pick, - intervalKey: string -): WorkerChartData => { - if (isIdleState(worker)) return { invocation: [], metrics: [] } - - const isErrored = worker.state === 'errored' - const seedBase = hashString(worker.name) * 1000 - const minutes = getIntervalMinutes(intervalKey) - const end = new Date(CHART_END_ISO) - end.setSeconds(0, 0) - - const invocation: WorkerInvocationDatum[] = Array.from({ length: minutes }, (_, index) => { - const timestamp = new Date(end) - timestamp.setMinutes(end.getMinutes() - (minutes - 1 - index)) - - const progress = minutes <= 1 ? 0 : index / (minutes - 1) - const baseLoad = (14 + Math.round(progress * 30)) * worker.instances - const noise = Math.round(pseudoNoise(seedBase + index * 0.1, 10)) - - const ok_count = Math.max(0, baseLoad + noise) - const warning_count = index % 15 === 0 ? 2 + (index % 3) : index % 9 === 0 ? 1 : 0 - const error_count = isErrored - ? index % 4 === 0 - ? 4 + (index % 5) - : 1 - : index % 47 === 0 - ? 2 - : 0 - - return { timestamp: timestamp.toISOString(), ok_count, warning_count, error_count } - }) - - const metrics: WorkerMetricsDatum[] = invocation.map((datum, index) => { - const avg_response_time = Math.round(42 + pseudoNoise(seedBase + index + 2, 26) + index * 0.12) - const max_response_time = Math.round( - avg_response_time * (1.3 + pseudoNoise(seedBase + index + minutes, 0.4)) - ) - - return { - ...datum, - avg_response_time, - max_response_time, - avg_cpu_time_used: Math.round(9 + pseudoNoise(seedBase + index + 4, 11) + index * 0.04), - max_cpu_time_used: Math.round(18 + pseudoNoise(seedBase + index + 6, 20) + index * 0.08), - avg_memory_used: Number( - ((worker.state === 'errored' ? 55 : 42) + pseudoNoise(seedBase + index + 8, 14)).toFixed(1) - ), - memory_percent: Number((0.4 + pseudoNoise(seedBase + index + 9, 0.25)).toFixed(3)), - } - }) - - return { invocation, metrics } -} - -export const formatRate = (count: number, total: number) => - new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 1 }).format( - total === 0 ? 0 : count / total - ) - -export const formatMetric = (value: number, unit?: string) => { - const formatted = unit === 'MB' ? value.toFixed(1) : Math.round(value).toLocaleString('en-US') - return unit ? `${formatted}${unit}` : formatted -} - -export const getSegmentedButtonClassName = (index: number, total: number) => { - if (index === 0) return 'rounded-tr-none rounded-br-none' - if (index === total - 1) return 'rounded-tl-none rounded-bl-none' - return 'rounded-none' -} - -export const sumBy = (items: T[], getValue: (item: T) => number) => - items.reduce((total, item) => total + getValue(item), 0) - -export const meanBy = (items: T[], getValue: (item: T) => number) => - items.length === 0 ? 0 : sumBy(items, getValue) / items.length - -export const RESPONSE_TIME_CHART_CONFIG = { - avg_response_time: { label: 'Average Response Time', color: 'var(--foreground-default)' }, - max_response_time: { label: 'Max Response Time', color: 'hsl(var(--brand-default))' }, -} satisfies ChartConfig - -export const CPU_TIME_CHART_CONFIG = { - max_cpu_time_used: { label: 'Max CPU Time', color: 'hsl(var(--brand-default))' }, -} satisfies ChartConfig - -export const MEMORY_CHART_CONFIG = { - avg_memory_used: { label: 'Memory Usage', color: 'hsl(var(--brand-default))' }, -} satisfies ChartConfig