diff --git a/spec/openapi.dashboard-api.yaml b/spec/openapi.dashboard-api.yaml
index 24c59aa12..2a725c054 100644
--- a/spec/openapi.dashboard-api.yaml
+++ b/spec/openapi.dashboard-api.yaml
@@ -66,6 +66,24 @@ components:
description: Cursor returned by the previous list response in `created_at|build_id` format.
schema:
type: string
+ builds_cpu_count:
+ name: cpuCount
+ in: query
+ required: false
+ description: Filter builds by exact vCPU count.
+ schema:
+ type: integer
+ format: int32
+ minimum: 1
+ builds_memory_mb:
+ name: memoryMB
+ in: query
+ required: false
+ description: Filter builds by exact memory size in MB.
+ schema:
+ type: integer
+ format: int32
+ minimum: 1
build_id_or_template:
name: build_id_or_template
in: query
@@ -905,6 +923,8 @@ paths:
parameters:
- $ref: "#/components/parameters/build_id_or_template"
- $ref: "#/components/parameters/build_statuses"
+ - $ref: "#/components/parameters/builds_cpu_count"
+ - $ref: "#/components/parameters/builds_memory_mb"
- $ref: "#/components/parameters/builds_limit"
- $ref: "#/components/parameters/builds_cursor"
responses:
diff --git a/src/core/modules/builds/models.ts b/src/core/modules/builds/models.ts
index e94eb62c1..463d10552 100644
--- a/src/core/modules/builds/models.ts
+++ b/src/core/modules/builds/models.ts
@@ -22,6 +22,7 @@ type _BuildStatusExhaustiveCheck = AssertTrue<
// TypeCheck: End
export const BuildStatusSchema = z.enum(BUILD_STATUS_VALUES)
+
export interface ListedBuildModel {
id: string
// id or alias
@@ -31,6 +32,10 @@ export interface ListedBuildModel {
statusMessage: string | null
createdAt: number
finishedAt: number | null
+ cpuCount: number
+ memoryMB: number
+ diskSizeMB: number | null
+ envdVersion: string | null
}
export interface RunningBuildStatusModel {
diff --git a/src/core/modules/builds/repository.server.ts b/src/core/modules/builds/repository.server.ts
index 1cfaab6d9..f9a234a42 100644
--- a/src/core/modules/builds/repository.server.ts
+++ b/src/core/modules/builds/repository.server.ts
@@ -57,6 +57,8 @@ function normalizeListBuildsLimit(limit?: number): number {
}
interface ListBuildsOptions {
+ cpuCount?: number
+ memoryMB?: number
limit?: number
cursor?: string
}
@@ -101,6 +103,8 @@ export function createBuildsRepository(
query: {
build_id_or_template: buildIdOrTemplate?.trim() || undefined,
statuses,
+ cpuCount: options.cpuCount,
+ memoryMB: options.memoryMB,
limit,
cursor: options.cursor,
},
@@ -149,6 +153,10 @@ export function createBuildsRepository(
finishedAt: build.finishedAt
? new Date(build.finishedAt).getTime()
: null,
+ cpuCount: build.cpuCount,
+ memoryMB: build.memoryMB,
+ diskSizeMB: build.diskSizeMB,
+ envdVersion: build.envdVersion,
})
),
nextCursor: result.data?.nextCursor ?? null,
diff --git a/src/core/server/api/routers/builds.ts b/src/core/server/api/routers/builds.ts
index 98aab5bf8..536991f84 100644
--- a/src/core/server/api/routers/builds.ts
+++ b/src/core/server/api/routers/builds.ts
@@ -29,17 +29,22 @@ export const buildsRouter = createTRPCRouter({
z.object({
buildIdOrTemplate: z.string().optional(),
statuses: z.array(BuildStatusSchema),
+ cpuCount: z.number().int().min(1).optional(),
+ memoryMB: z.number().int().min(1).optional(),
limit: z.number().min(1).max(100).default(50),
cursor: z.string().optional(),
})
)
.query(async ({ ctx, input }) => {
- const { buildIdOrTemplate, statuses, limit, cursor } = input
+ const { buildIdOrTemplate, statuses, cpuCount, memoryMB, limit, cursor } =
+ input
const result = await ctx.buildsRepository.listBuilds(
buildIdOrTemplate,
statuses,
{
+ cpuCount,
+ memoryMB,
limit,
cursor,
}
diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts
index 562a37e03..b42f24832 100644
--- a/src/core/shared/contracts/dashboard-api.types.ts
+++ b/src/core/shared/contracts/dashboard-api.types.ts
@@ -55,6 +55,10 @@ export interface paths {
build_id_or_template?: components['parameters']['build_id_or_template']
/** @description Comma-separated list of build statuses to include. */
statuses?: components['parameters']['build_statuses']
+ /** @description Filter builds by exact vCPU count. */
+ cpuCount?: components['parameters']['builds_cpu_count']
+ /** @description Filter builds by exact memory size in MB. */
+ memoryMB?: components['parameters']['builds_memory_mb']
/** @description Maximum number of items to return per page. */
limit?: components['parameters']['builds_limit']
/** @description Cursor returned by the previous list response in `created_at|build_id` format. */
@@ -1229,6 +1233,10 @@ export interface components {
builds_limit: number
/** @description Cursor returned by the previous list response in `created_at|build_id` format. */
builds_cursor: string
+ /** @description Filter builds by exact vCPU count. */
+ builds_cpu_count: number
+ /** @description Filter builds by exact memory size in MB. */
+ builds_memory_mb: number
/** @description Optional filter by build identifier, template identifier, or template alias. */
build_id_or_template: string
/** @description Comma-separated list of build statuses to include. */
diff --git a/src/features/dashboard/common/envd-version.tsx b/src/features/dashboard/common/envd-version.tsx
new file mode 100644
index 000000000..6d9cf11f0
--- /dev/null
+++ b/src/features/dashboard/common/envd-version.tsx
@@ -0,0 +1,39 @@
+import { cn } from '@/lib/utils'
+import { isVersionCompatible } from '@/lib/utils/version'
+import HelpTooltip from '@/ui/help-tooltip'
+
+const INVALID_ENVD_VERSION = '0.0.1'
+const SDK_V2_MINIMAL_ENVD_VERSION = '0.2.0'
+
+export function EnvdVersion({
+ version,
+ className,
+}: {
+ version: string | null | undefined
+ className?: string
+}) {
+ const versionValue =
+ version && version !== INVALID_ENVD_VERSION ? version : null
+
+ const isNotV2Compatible = versionValue
+ ? isVersionCompatible(versionValue, SDK_V2_MINIMAL_ENVD_VERSION) === false
+ : false
+
+ return (
+
+ {versionValue ?? '--'}
+ {isNotV2Compatible && (
+
+ The envd version is not compatible with the SDK v2. To update the envd
+ version, you need to rebuild the template.
+
+ )}
+
+ )
+}
diff --git a/src/features/dashboard/common/resources-filter.tsx b/src/features/dashboard/common/resources-filter.tsx
new file mode 100644
index 000000000..f8a0902b6
--- /dev/null
+++ b/src/features/dashboard/common/resources-filter.tsx
@@ -0,0 +1,103 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { NumberInput } from '@/ui/number-input'
+import { Button } from '@/ui/primitives/button'
+import { Label } from '@/ui/primitives/label'
+import { Separator } from '@/ui/primitives/separator'
+
+export interface ResourcesFilterValue {
+ cpuCount?: number
+ memoryMB?: number
+}
+
+interface ResourcesFilterProps {
+ value: ResourcesFilterValue
+ onChange: (value: ResourcesFilterValue) => void
+ className?: string
+}
+
+const formatMemoryDisplay = (memoryValue: number) => {
+ if (memoryValue === 0) return 'Unfiltered'
+
+ return memoryValue < 1024 ? `${memoryValue} MB` : `${memoryValue / 1024} GB`
+}
+
+/**
+ * State-agnostic CPU + memory filter. The parent owns the value (URL params,
+ * a store, …) and is responsible for any debouncing.
+ */
+export function ResourcesFilter({
+ value,
+ onChange,
+ className,
+}: ResourcesFilterProps) {
+ const cpu = value.cpuCount ?? 0
+ const memory = value.memoryMB ?? 0
+
+ const handleCpuChange = (next: number) => {
+ onChange({ cpuCount: next || undefined, memoryMB: value.memoryMB })
+ }
+
+ const handleMemoryChange = (next: number) => {
+ onChange({ cpuCount: value.cpuCount, memoryMB: next || undefined })
+ }
+
+ return (
+
- {versionValue ?? '--'}
- {isNotV2Compatible && (
-
- The envd version is not compatible with the SDK v2. To update the envd
- version, you need to rebuild the template.
-
- )}
-