From 24483b6a166a610ff42d18ef8f67e7ddd07ac994 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 4 Jun 2026 14:26:23 -0400 Subject: [PATCH 1/4] feat(sandbox): add killSandbox functionality and update UI integration - Introduced `killSandbox` method in the `SandboxesRepository` to handle sandbox deletion. - Removed the deprecated `sandbox-actions.ts` file and migrated the kill action to the TRPC router. - Updated the `KillButton` component to use `useMutation` from React Query for improved state management and error handling. - Enhanced user feedback with toast notifications for success and error scenarios. This change streamlines the sandbox management process and aligns with the new API structure. --- .../modules/sandboxes/repository.server.ts | 43 +++++++++++++ src/core/server/actions/sandbox-actions.ts | 63 ------------------- src/core/server/api/routers/sandbox.ts | 10 +++ .../dashboard/sandbox/header/kill-button.tsx | 37 ++++++----- 4 files changed, 73 insertions(+), 80 deletions(-) delete mode 100644 src/core/server/actions/sandbox-actions.ts diff --git a/src/core/modules/sandboxes/repository.server.ts b/src/core/modules/sandboxes/repository.server.ts index 2f4ad1aba..54caa3bb0 100644 --- a/src/core/modules/sandboxes/repository.server.ts +++ b/src/core/modules/sandboxes/repository.server.ts @@ -60,6 +60,7 @@ export interface SandboxesRepository { sandboxId: string, options: GetSandboxMetricsOptions ): Promise> + killSandbox(sandboxId: string): Promise> listSandboxes(): Promise> getSandboxesMetrics( sandboxIds: string[] @@ -356,6 +357,48 @@ export function createSandboxesRepository( return ok(result.data) }, + async killSandbox(sandboxId) { + const result = await deps.infraClient.DELETE('/sandboxes/{sandboxID}', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + params: { + path: { + sandboxID: sandboxId, + }, + }, + }) + + if (!result.response.ok || result.error) { + const status = result.response.status + + l.error( + { + key: 'repositories:sandboxes:kill_sandbox:infra_error', + error: result.error, + team_id: scope.teamId, + context: { + status, + path: '/sandboxes/{sandboxID}', + sandbox_id: sandboxId, + }, + }, + `failed to delete /sandboxes/{sandboxID}: ${result.error?.message || 'Unknown error'}` + ) + + return err( + repoErrorFromHttp( + status, + status === 404 + ? 'Sandbox not found' + : (result.error?.message ?? 'Failed to kill sandbox'), + result.error + ) + ) + } + + return ok(undefined) + }, async listSandboxes() { const result = await deps.infraClient.GET('/sandboxes', { headers: { diff --git a/src/core/server/actions/sandbox-actions.ts b/src/core/server/actions/sandbox-actions.ts deleted file mode 100644 index b0c35cb27..000000000 --- a/src/core/server/actions/sandbox-actions.ts +++ /dev/null @@ -1,63 +0,0 @@ -'use server' - -import { updateTag } from 'next/cache' -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { CACHE_TAGS } from '@/configs/cache' -import { - authActionClient, - withTeamSlugResolution, -} from '@/core/server/actions/client' -import { returnServerError } from '@/core/server/actions/utils' -import { infra } from '@/core/shared/clients/api' -import { l } from '@/core/shared/clients/logger/logger' -import { TeamSlugSchema } from '@/core/shared/schemas/team' - -const KillSandboxSchema = z.object({ - teamSlug: TeamSlugSchema, - sandboxId: z.string().min(1, 'Sandbox ID is required'), -}) - -export const killSandboxAction = authActionClient - .schema(KillSandboxSchema) - .metadata({ actionName: 'killSandbox' }) - .use(withTeamSlugResolution) - .action(async ({ parsedInput, ctx }) => { - const { sandboxId } = parsedInput - const { session, teamId } = ctx - - const res = await infra.DELETE('/sandboxes/{sandboxID}', { - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - params: { - path: { - sandboxID: sandboxId, - }, - }, - }) - - if (res.error) { - const status = res.response.status - - l.error( - { - key: 'kill_sandbox_action:infra_error', - error: res.error, - user_id: session.user.id, - team_id: teamId, - sandbox_id: sandboxId, - context: { - status, - }, - }, - `Failed to kill sandbox: ${res.error.message}` - ) - - if (status === 404) { - return returnServerError('Sandbox not found') - } - - return returnServerError('Failed to kill sandbox') - } - }) diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index 948367c59..ba083ea82 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -205,4 +205,14 @@ export const sandboxRouter = createTRPCRouter({ }), // MUTATIONS + kill: sandboxRepositoryProcedure + .input( + z.object({ + sandboxId: SandboxIdSchema, + }) + ) + .mutation(async ({ ctx, input }) => { + const result = await ctx.sandboxesRepository.killSandbox(input.sandboxId) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + }), }) diff --git a/src/features/dashboard/sandbox/header/kill-button.tsx b/src/features/dashboard/sandbox/header/kill-button.tsx index f177191bf..94c6b8111 100644 --- a/src/features/dashboard/sandbox/header/kill-button.tsx +++ b/src/features/dashboard/sandbox/header/kill-button.tsx @@ -1,9 +1,9 @@ 'use client' -import { useAction } from 'next-safe-action/hooks' +import { useMutation } from '@tanstack/react-query' import { useState } from 'react' import { toast } from 'sonner' -import { killSandboxAction } from '@/core/server/actions/sandbox-actions' +import { useTRPC } from '@/trpc/client' import { AlertPopover } from '@/ui/alert-popover' import { Button } from '@/ui/primitives/button' import { TrashIcon } from '@/ui/primitives/icons' @@ -18,27 +18,30 @@ export default function KillButton({ className }: KillButtonProps) { const [open, setOpen] = useState(false) const { sandboxInfo, refetchSandboxInfo } = useSandboxContext() const { team } = useDashboard() + const trpc = useTRPC() const canKill = Boolean( sandboxInfo?.sandboxID && sandboxInfo.state !== 'killed' ) - const { execute, isExecuting } = useAction(killSandboxAction, { - onSuccess: async () => { - toast.success('Sandbox killed successfully') - setOpen(false) - refetchSandboxInfo() - }, - onError: ({ error }) => { - toast.error( - error.serverError || 'Failed to kill sandbox. Please try again.' - ) - }, - }) + const killSandboxMutation = useMutation( + trpc.sandbox.kill.mutationOptions({ + onSuccess: async () => { + toast.success('Sandbox killed successfully') + setOpen(false) + refetchSandboxInfo() + }, + onError: (error) => { + toast.error( + error.message || 'Failed to kill sandbox. Please try again.' + ) + }, + }) + ) const handleKill = () => { if (!canKill || !sandboxInfo?.sandboxID) return - execute({ + killSandboxMutation.mutate({ teamSlug: team.slug, sandboxId: sandboxInfo.sandboxID, }) @@ -58,8 +61,8 @@ export default function KillButton({ className }: KillButtonProps) { } confirmProps={{ - disabled: isExecuting, - loading: isExecuting ? 'Killing...' : undefined, + disabled: killSandboxMutation.isPending, + loading: killSandboxMutation.isPending ? 'Killing...' : undefined, }} onConfirm={handleKill} onCancel={() => setOpen(false)} From 9af044753ee531e62fc7ff25953131e113835d00 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 4 Jun 2026 14:56:29 -0400 Subject: [PATCH 2/4] feat(account): implement user access token retrieval via TRPC - Removed the deprecated `getUserAccessTokenAction` from user-actions. - Introduced a new `accountRouter` with a `getUserAccessToken` mutation to handle access token retrieval. - Updated the `UserAccessToken` component to use `useMutation` from React Query for improved state management and error handling. This change enhances the API structure and aligns with the new TRPC approach for user actions. --- src/core/server/actions/user-actions.ts | 11 -------- src/core/server/api/routers/account.ts | 25 +++++++++++++++++++ src/core/server/api/routers/index.ts | 2 ++ .../dashboard/account/user-access-token.tsx | 24 ++++++++---------- 4 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 src/core/server/api/routers/account.ts diff --git a/src/core/server/actions/user-actions.ts b/src/core/server/actions/user-actions.ts index 78a97eb7b..fcd4c9217 100644 --- a/src/core/server/actions/user-actions.ts +++ b/src/core/server/actions/user-actions.ts @@ -8,7 +8,6 @@ import { authActionClient } from '@/core/server/actions/client' import { auth } from '@/core/server/auth' import { supabaseAuthFlows } from '@/core/server/auth/supabase/flows' import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' -import { generateE2BUserAccessToken } from '@/lib/utils/server' const UpdateUserSchema = z .object({ @@ -128,13 +127,3 @@ export const updateUserAction = authActionClient throw error } }) - -export const getUserAccessTokenAction = authActionClient - .metadata({ actionName: 'getUserAccessToken' }) - .action(async ({ ctx }) => { - const { session } = ctx - - const token = await generateE2BUserAccessToken(session.access_token) - - return token - }) diff --git a/src/core/server/api/routers/account.ts b/src/core/server/api/routers/account.ts new file mode 100644 index 000000000..cb1b884aa --- /dev/null +++ b/src/core/server/api/routers/account.ts @@ -0,0 +1,25 @@ +import { TRPCError } from '@trpc/server' +import { ActionError } from '@/core/server/actions/utils' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { protectedProcedure } from '@/core/server/trpc/procedures' +import { generateE2BUserAccessToken } from '@/lib/utils/server' + +const accountRouter = createTRPCRouter({ + getUserAccessToken: protectedProcedure.mutation(async ({ ctx }) => { + try { + return await generateE2BUserAccessToken(ctx.session.access_token) + } catch (error) { + if (error instanceof ActionError) { + throw new TRPCError({ + code: error.expected ? 'BAD_REQUEST' : 'INTERNAL_SERVER_ERROR', + message: error.message, + cause: error.cause, + }) + } + + throw error + } + }), +}) + +export { accountRouter } diff --git a/src/core/server/api/routers/index.ts b/src/core/server/api/routers/index.ts index 8c5cce9ed..ad0249181 100644 --- a/src/core/server/api/routers/index.ts +++ b/src/core/server/api/routers/index.ts @@ -1,4 +1,5 @@ import { createCallerFactory, createTRPCRouter } from '@/core/server/trpc/init' +import { accountRouter } from './account' import { billingRouter } from './billing' import { buildsRouter } from './builds' import { sandboxRouter } from './sandbox' @@ -9,6 +10,7 @@ import { templatesRouter } from './templates' import { webhooksRouter } from './webhooks' export const trpcAppRouter = createTRPCRouter({ + account: accountRouter, sandbox: sandboxRouter, sandboxes: sandboxesRouter, templates: templatesRouter, diff --git a/src/features/dashboard/account/user-access-token.tsx b/src/features/dashboard/account/user-access-token.tsx index 433b26c22..0c69d353a 100644 --- a/src/features/dashboard/account/user-access-token.tsx +++ b/src/features/dashboard/account/user-access-token.tsx @@ -1,9 +1,9 @@ 'use client' -import { useAction } from 'next-safe-action/hooks' +import { useMutation } from '@tanstack/react-query' import { useState } from 'react' -import { getUserAccessTokenAction } from '@/core/server/actions/user-actions' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' +import { useTRPC } from '@/trpc/client' import CopyButton from '@/ui/copy-button' import { IconButton } from '@/ui/primitives/icon-button' import { EyeIcon, EyeOffIcon } from '@/ui/primitives/icons' @@ -16,22 +16,20 @@ interface UserAccessTokenProps { export default function UserAccessToken({ className }: UserAccessTokenProps) { const { toast } = useToast() + const trpc = useTRPC() const [token, setToken] = useState() const [isVisible, setIsVisible] = useState(false) - const { execute: fetchToken, isPending } = useAction( - getUserAccessTokenAction, - { + const getUserAccessTokenMutation = useMutation( + trpc.account.getUserAccessToken.mutationOptions({ onSuccess: (result) => { - if (result.data) { - setToken(result.data.token) - setIsVisible(true) - } + setToken(result.token) + setIsVisible(true) }, onError: () => { toast(defaultErrorToast('Failed to fetch access token')) }, - } + }) ) return ( @@ -51,12 +49,12 @@ export default function UserAccessToken({ className }: UserAccessTokenProps) { setIsVisible(!isVisible) setToken(undefined) } else { - fetchToken() + getUserAccessTokenMutation.mutate() } }} - disabled={isPending} + disabled={getUserAccessTokenMutation.isPending} > - {isPending ? ( + {getUserAccessTokenMutation.isPending ? ( ) : token ? ( isVisible ? ( From d1556f98ab2cac9f66da22bc8c2b7fab0f385cb4 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 4 Jun 2026 15:23:41 -0400 Subject: [PATCH 3/4] refactor(usage): migrate usage data retrieval to TRPC and enhance error handling - Replaced the deprecated `getUsage` function with a TRPC call to `billing.getUsage` for fetching usage data. - Updated the `UsagePage` component to handle errors more gracefully, providing clearer error messages. - Removed the obsolete `get-usage.ts` file to streamline the codebase. This change aligns the usage data retrieval with the new TRPC architecture and improves the user experience during data loading failures. --- src/app/dashboard/[teamSlug]/usage/page.tsx | 88 ++++++++++---------- src/core/server/functions/usage/get-usage.ts | 41 --------- 2 files changed, 46 insertions(+), 83 deletions(-) delete mode 100644 src/core/server/functions/usage/get-usage.ts diff --git a/src/app/dashboard/[teamSlug]/usage/page.tsx b/src/app/dashboard/[teamSlug]/usage/page.tsx index aa80e4d88..3662adf41 100644 --- a/src/app/dashboard/[teamSlug]/usage/page.tsx +++ b/src/app/dashboard/[teamSlug]/usage/page.tsx @@ -1,6 +1,6 @@ -import { getUsage } from '@/core/server/functions/usage/get-usage' import { UsageChartsProvider } from '@/features/dashboard/usage/usage-charts-context' import { UsageMetricChart } from '@/features/dashboard/usage/usage-metric-chart' +import { trpcCaller } from '@/trpc/server' import ErrorBoundary from '@/ui/error' import Frame from '@/ui/frame' @@ -10,57 +10,61 @@ export default async function UsagePage({ params: Promise<{ teamSlug: string }> }) { const { teamSlug } = await params - const result = await getUsage({ teamSlug }) - if (!result?.data || result.serverError || result.validationErrors) { + try { + const usageData = await trpcCaller.billing.getUsage({ teamSlug }) + + return ( + +
+
+ +
+ + + + +
+ +
+
+
+ ) + } catch (error) { return ( ) } - - return ( - -
-
- -
- - - - -
- -
-
-
- ) } diff --git a/src/core/server/functions/usage/get-usage.ts b/src/core/server/functions/usage/get-usage.ts deleted file mode 100644 index e01c4aeaa..000000000 --- a/src/core/server/functions/usage/get-usage.ts +++ /dev/null @@ -1,41 +0,0 @@ -import 'server-only' - -import { cacheLife, cacheTag } from 'next/cache' -import { z } from 'zod' -import { CACHE_TAGS } from '@/configs/cache' -import { createBillingRepository } from '@/core/modules/billing/repository.server' -import { - authActionClient, - withTeamSlugResolution, -} from '@/core/server/actions/client' -import { returnServerError } from '@/core/server/actions/utils' -import { getPublicRepoErrorMessage } from '@/core/shared/errors' -import { TeamSlugSchema } from '@/core/shared/schemas/team' - -const GetUsageAuthActionSchema = z.object({ - teamSlug: TeamSlugSchema, -}) - -export const getUsage = authActionClient - .schema(GetUsageAuthActionSchema) - .metadata({ serverFunctionName: 'getUsage' }) - .use(withTeamSlugResolution) - .action(async ({ ctx }) => { - 'use cache' - - const { teamId } = ctx - - cacheLife('hours') - cacheTag(CACHE_TAGS.TEAM_USAGE(teamId)) - - const result = await createBillingRepository({ - accessToken: ctx.session.access_token, - teamId, - }).getUsage() - - if (!result.ok) { - return returnServerError(getPublicRepoErrorMessage(result.error)) - } - - return result.data - }) From 052939430034bb91678d4b5abbaed9f11baed0fc Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 4 Jun 2026 15:35:14 -0400 Subject: [PATCH 4/4] refactor(metrics): migrate team metrics retrieval to TRPC and remove deprecated functions - Deleted the `getTeamMetrics` and `getTeamMetricsMax` functions in favor of TRPC calls. - Updated components to utilize the new TRPC structure for fetching team metrics. - Enhanced error handling in the UI to provide clearer feedback during data loading failures. This change aligns the metrics retrieval process with the new TRPC architecture, improving maintainability and user experience. --- .../sandboxes/get-team-metrics-max.ts | 117 ------------------ .../functions/sandboxes/get-team-metrics.ts | 72 ----------- .../sandboxes/live-counter.client.tsx | 8 +- .../sandboxes/live-counter.server.tsx | 32 ++--- .../sandboxes/monitoring/charts/charts.tsx | 36 +++--- .../sandboxes/monitoring/header.client.tsx | 8 +- .../dashboard/sandboxes/monitoring/header.tsx | 74 +++++------ 7 files changed, 72 insertions(+), 275 deletions(-) delete mode 100644 src/core/server/functions/sandboxes/get-team-metrics-max.ts delete mode 100644 src/core/server/functions/sandboxes/get-team-metrics.ts diff --git a/src/core/server/functions/sandboxes/get-team-metrics-max.ts b/src/core/server/functions/sandboxes/get-team-metrics-max.ts deleted file mode 100644 index 0a2078f8a..000000000 --- a/src/core/server/functions/sandboxes/get-team-metrics-max.ts +++ /dev/null @@ -1,117 +0,0 @@ -import 'server-only' - -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { USE_MOCK_DATA } from '@/configs/flags' -import { MOCK_TEAM_METRICS_MAX_DATA } from '@/configs/mock-data' -import { - authActionClient, - withTeamSlugResolution, -} from '@/core/server/actions/client' -import { handleDefaultInfraError } from '@/core/server/actions/utils' -import { infra } from '@/core/shared/clients/api' -import { l } from '@/core/shared/clients/logger/logger' -import { TeamSlugSchema } from '@/core/shared/schemas/team' -import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' - -export const GetTeamMetricsMaxSchema = z - .object({ - teamSlug: TeamSlugSchema, - startDate: z - .number() - .int() - .positive() - .describe('Unix timestamp in milliseconds') - .refine( - (start) => { - const now = Date.now() - - return start >= now - MAX_DAYS_AGO - }, - { - message: `Start date cannot be more than ${MAX_DAYS_AGO / (1000 * 60 * 60 * 24)} days ago`, - } - ), - endDate: z - .number() - .int() - .positive() - .describe('Unix timestamp in milliseconds') - .refine((end) => end <= Date.now(), { - message: 'End date cannot be in the future', - }), - metric: z.enum(['concurrent_sandboxes', 'sandbox_start_rate']), - }) - .refine( - (data) => { - return data.endDate - data.startDate <= MAX_DAYS_AGO - }, - { - message: `Date range cannot exceed ${MAX_DAYS_AGO / (1000 * 60 * 60 * 24)} days`, - } - ) - -export const getTeamMetricsMax = authActionClient - .metadata({ serverFunctionName: 'getTeamMetricsMax' }) - .schema(GetTeamMetricsMaxSchema) - .use(withTeamSlugResolution) - .action(async ({ parsedInput, ctx }) => { - const { session, teamId } = ctx - const { startDate: startDateMs, endDate: endDateMs, metric } = parsedInput - - if (USE_MOCK_DATA) { - return MOCK_TEAM_METRICS_MAX_DATA(startDateMs, endDateMs, metric) - } - - // convert milliseconds to seconds for the API - const startSeconds = Math.floor(startDateMs / 1000) - const endSeconds = Math.floor(endDateMs / 1000) - - const res = await infra.GET('/teams/{teamID}/metrics/max', { - params: { - path: { - teamID: teamId, - }, - query: { - start: startSeconds, - end: endSeconds, - metric, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - cache: 'no-store', - }) - - if (res.error) { - const status = res.response.status - - l.error( - { - key: 'get_team_metrics_max:infra_error', - error: res.error, - team_id: teamId, - user_id: session.user.id, - context: { - status, - startDate: startDateMs, - endDate: endDateMs, - metric, - }, - }, - `Failed to get team metrics max: ${res.error.message}` - ) - - return handleDefaultInfraError(status, res.error) - } - - // since javascript timestamps are in milliseconds, we want to convert the timestamp back to milliseconds - const timestampMs = res.data.timestampUnix * 1000 - - return { - timestamp: timestampMs, - value: res.data.value, - metric, - } - }) diff --git a/src/core/server/functions/sandboxes/get-team-metrics.ts b/src/core/server/functions/sandboxes/get-team-metrics.ts deleted file mode 100644 index cc90ddacf..000000000 --- a/src/core/server/functions/sandboxes/get-team-metrics.ts +++ /dev/null @@ -1,72 +0,0 @@ -import 'server-only' - -import { z } from 'zod' -import { - authActionClient, - withTeamSlugResolution, -} from '@/core/server/actions/client' -import { returnServerError } from '@/core/server/actions/utils' -import { getPublicErrorMessage } from '@/core/shared/errors' -import { TeamSlugSchema } from '@/core/shared/schemas/team' -import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { getTeamMetricsCore } from './get-team-metrics-core' - -export const GetTeamMetricsSchema = z - .object({ - teamSlug: TeamSlugSchema, - startDate: z - .number() - .int() - .positive() - .describe('Unix timestamp in milliseconds') - .refine( - (start) => { - const now = Date.now() - - return start >= now - MAX_DAYS_AGO - }, - { - message: `Start date cannot be more than ${MAX_DAYS_AGO / (1000 * 60 * 60 * 24)} days ago`, - } - ), - endDate: z - .number() - .int() - .positive() - .describe('Unix timestamp in milliseconds') - .refine((end) => end <= Date.now(), { - message: 'End date cannot be in the future', - }), - }) - .refine( - (data) => { - return data.endDate - data.startDate <= MAX_DAYS_AGO - }, - { - message: `Date range cannot exceed ${MAX_DAYS_AGO / (1000 * 60 * 60 * 24)} days`, - } - ) - -export const getTeamMetrics = authActionClient - .schema(GetTeamMetricsSchema) - .metadata({ serverFunctionName: 'getTeamMetrics' }) - .use(withTeamSlugResolution) - .action(async ({ parsedInput, ctx }) => { - const { session, teamId } = ctx - - const { startDate: startDateMs, endDate: endDateMs } = parsedInput - - const result = await getTeamMetricsCore({ - accessToken: session.access_token, - teamId, - userId: session.user.id, - startMs: startDateMs, - endMs: endDateMs, - }) - - if (result.error) { - return returnServerError(getPublicErrorMessage({ status: result.status })) - } - - return result.data - }) diff --git a/src/features/dashboard/sandboxes/live-counter.client.tsx b/src/features/dashboard/sandboxes/live-counter.client.tsx index e49977db2..ffc61d598 100644 --- a/src/features/dashboard/sandboxes/live-counter.client.tsx +++ b/src/features/dashboard/sandboxes/live-counter.client.tsx @@ -1,15 +1,11 @@ 'use client' -import type { InferSafeActionFnResult } from 'next-safe-action' -import type { NonUndefined } from 'react-hook-form' -import type { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' +import type { TRPCRouterOutputs } from '@/trpc/client' import { LiveSandboxCounter } from './live-counter' import { useRecentMetrics } from './monitoring/hooks/use-recent-metrics' interface LiveSandboxCounterClientProps { - initialData: NonUndefined< - InferSafeActionFnResult['data'] - > + initialData: TRPCRouterOutputs['sandboxes']['getTeamMetrics'] className?: string } diff --git a/src/features/dashboard/sandboxes/live-counter.server.tsx b/src/features/dashboard/sandboxes/live-counter.server.tsx index 5f87fdda4..47ebe1154 100644 --- a/src/features/dashboard/sandboxes/live-counter.server.tsx +++ b/src/features/dashboard/sandboxes/live-counter.server.tsx @@ -1,8 +1,8 @@ import { Suspense } from 'react' -import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' -import { l } from '@/core/shared/clients/logger/logger' +import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' import { cn } from '@/lib/utils' import { getNowMemo } from '@/lib/utils/server' +import { trpcCaller } from '@/trpc/server' import { Skeleton } from '@/ui/primitives/skeleton' import { LiveSandboxCounterClient } from './live-counter.client' @@ -36,19 +36,26 @@ async function LiveSandboxCounterResolver({ const now = getNowMemo() const start = now - 60_000 - const teamMetricsResult = await getTeamMetrics({ - teamSlug, - startDate: start, - endDate: now, - }) + try { + const teamMetrics = await trpcCaller.sandboxes.getTeamMetrics({ + teamSlug, + startDate: start, + endDate: now, + }) - if (!teamMetricsResult?.data || teamMetricsResult.serverError) { + return ( + + ) + } catch (error) { l.error( { key: 'live_sandbox_counter:error', + error: serializeErrorForLog(error), context: { teamSlug, - serverError: teamMetricsResult?.serverError, }, }, 'Failed to load live sandbox count' @@ -56,11 +63,4 @@ async function LiveSandboxCounterResolver({ return null } - - return ( - - ) } diff --git a/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx b/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx index 4d1b5551a..bc0e621df 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx +++ b/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx @@ -1,6 +1,6 @@ import { Suspense } from 'react' import { TEAM_METRICS_INITIAL_RANGE_MS } from '@/configs/intervals' -import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' +import { trpcCaller } from '@/trpc/server' import { TeamMetricsChartsProvider } from '../charts-context' import ConcurrentChartClient from './concurrent-chart' import ChartFallback from './fallback' @@ -46,21 +46,22 @@ async function TeamMetricsChartsResolver({ : now - TEAM_METRICS_INITIAL_RANGE_MS const end = endParam ? parseInt(endParam, 10) : now - const teamMetricsResult = await getTeamMetrics({ - teamSlug, - startDate: start, - endDate: end, - }) + try { + const teamMetrics = await trpcCaller.sandboxes.getTeamMetrics({ + teamSlug, + startDate: start, + endDate: end, + }) - if ( - !teamMetricsResult?.data || - teamMetricsResult.serverError || - teamMetricsResult.validationErrors - ) { + return ( + + + + + ) + } catch (error) { const errorMessage = - teamMetricsResult?.serverError || - teamMetricsResult?.validationErrors?.formErrors[0] || - 'Failed to load metrics data.' + error instanceof Error ? error.message : 'Failed to load metrics data.' return ( <> @@ -77,11 +78,4 @@ async function TeamMetricsChartsResolver({ ) } - - return ( - - - - - ) } diff --git a/src/features/dashboard/sandboxes/monitoring/header.client.tsx b/src/features/dashboard/sandboxes/monitoring/header.client.tsx index b66c82ded..82cb6f8df 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.client.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.client.tsx @@ -1,18 +1,14 @@ 'use client' -import type { InferSafeActionFnResult } from 'next-safe-action' import { useMemo } from 'react' -import type { NonUndefined } from 'react-hook-form' -import type { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' import { useDashboard } from '@/features/dashboard/context' import { formatDecimal, formatNumber } from '@/lib/utils/formatting' +import type { TRPCRouterOutputs } from '@/trpc/client' import { AnimatedNumber } from '@/ui/primitives/animated-number' import { useRecentMetrics } from './hooks/use-recent-metrics' interface TeamMonitoringHeaderClientProps { - initialData: NonUndefined< - InferSafeActionFnResult['data'] - > + initialData: TRPCRouterOutputs['sandboxes']['getTeamMetrics'] } export function ConcurrentSandboxesClient({ diff --git a/src/features/dashboard/sandboxes/monitoring/header.tsx b/src/features/dashboard/sandboxes/monitoring/header.tsx index 2e038efe0..9c991c49b 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.tsx @@ -1,7 +1,6 @@ import { Suspense } from 'react' -import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' -import { getTeamMetricsMax } from '@/core/server/functions/sandboxes/get-team-metrics-max' import { getNowMemo } from '@/lib/utils/server' +import { trpcCaller } from '@/trpc/server' import ErrorTooltip from '@/ui/error-tooltip' import { SemiLiveBadge } from '@/ui/live' import { WarningIcon } from '@/ui/primitives/icons' @@ -105,22 +104,23 @@ export const ConcurrentSandboxes = async ({ const now = getNowMemo() const start = now - 60_000 - const teamMetricsResult = await getTeamMetrics({ - teamSlug, - startDate: start, - endDate: now, - }) + try { + const teamMetrics = await trpcCaller.sandboxes.getTeamMetrics({ + teamSlug, + startDate: start, + endDate: now, + }) - if (!teamMetricsResult?.data || teamMetricsResult.serverError) { + return + } catch (error) { return ( - {teamMetricsResult?.serverError || - 'Failed to load concurrent sandboxes'} + {error instanceof Error + ? error.message + : 'Failed to load concurrent sandboxes'} ) } - - return } export const SandboxesStartRate = async ({ @@ -134,22 +134,23 @@ export const SandboxesStartRate = async ({ const now = getNowMemo() const start = now - 60_000 - const teamMetricsResult = await getTeamMetrics({ - teamSlug, - startDate: start, - endDate: now, - }) + try { + const teamMetrics = await trpcCaller.sandboxes.getTeamMetrics({ + teamSlug, + startDate: start, + endDate: now, + }) - if (!teamMetricsResult?.data || teamMetricsResult.serverError) { + return + } catch (error) { return ( - {teamMetricsResult?.serverError || - 'Failed to load max sandbox start rate'} + {error instanceof Error + ? error.message + : 'Failed to load max sandbox start rate'} ) } - - return } export const MaxConcurrentSandboxes = async ({ @@ -162,25 +163,24 @@ export const MaxConcurrentSandboxes = async ({ const end = Date.now() const start = end - (MAX_DAYS_AGO - 60_000) // 1 minute margin to avoid validation errors - const teamMetricsResult = await getTeamMetricsMax({ - teamSlug, - startDate: start, - endDate: end, - metric: 'concurrent_sandboxes', - }) + try { + const teamMetrics = await trpcCaller.sandboxes.getTeamMetricsMax({ + teamSlug, + startDate: start, + endDate: end, + metric: 'concurrent_sandboxes', + }) - if (!teamMetricsResult?.data || teamMetricsResult.serverError) { + return ( + + ) + } catch (error) { return ( - {teamMetricsResult?.serverError || - 'Failed to load max concurrent sandboxes'} + {error instanceof Error + ? error.message + : 'Failed to load max concurrent sandboxes'} ) } - - return ( - - ) }