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/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/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/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/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/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 - }) 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 ? ( 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)} 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 ( - - ) }