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