diff --git a/ui/src/api/queries/admin/environments.ts b/ui/src/api/queries/admin/environments.ts index 48d0b823..6949bb5f 100644 --- a/ui/src/api/queries/admin/environments.ts +++ b/ui/src/api/queries/admin/environments.ts @@ -54,6 +54,18 @@ export function useUpdateEnvironment() { }); } +export function useUpdateDefaultContainerConfig() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, config }: { id: string; config: Record }) => + adminFetch(`/environments/${id}/default-container-config`, { + method: 'PUT', + body: JSON.stringify(config), + }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'environments'] }), + }); +} + export function useDeleteEnvironment() { const qc = useQueryClient(); return useMutation({ diff --git a/ui/src/pages/Admin/EnvironmentsPage.tsx b/ui/src/pages/Admin/EnvironmentsPage.tsx index 58db7e9a..2b5206e4 100644 --- a/ui/src/pages/Admin/EnvironmentsPage.tsx +++ b/ui/src/pages/Admin/EnvironmentsPage.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Avatar, Badge, @@ -20,6 +20,7 @@ import { useCreateEnvironment, useUpdateEnvironment, useDeleteEnvironment, + useUpdateDefaultContainerConfig, } from '../../api/queries/admin/environments'; import type { Environment } from '../../api/queries/admin/environments'; import styles from './UserManagement.module.css'; @@ -42,6 +43,7 @@ export default function EnvironmentsPage() { const createEnv = useCreateEnvironment(); const updateEnv = useUpdateEnvironment(); const deleteEnv = useDeleteEnvironment(); + const updateDefaults = useUpdateDefaultContainerConfig(); const selected = useMemo( () => environments.find((e) => e.id === selectedId) ?? null, @@ -280,6 +282,15 @@ export default function EnvironmentsPage() {

)} + + { + try { + await updateDefaults.mutateAsync({ id: selected.id, config }); + toast({ title: 'Default resources updated', variant: 'success' }); + } catch { + toast({ title: 'Failed to update defaults', variant: 'error', duration: 86_400_000 }); + } + }} saving={updateDefaults.isPending} /> ) : null } @@ -297,3 +308,84 @@ export default function EnvironmentsPage() { ); } + +// ── Default Resource Limits ───────────────────────────────────────── + +function DefaultResourcesSection({ environment, onSave, saving }: { + environment: Environment; + onSave: (config: Record) => Promise; + saving: boolean; +}) { + const defaults = environment.defaultContainerConfig ?? {}; + const [editing, setEditing] = useState(false); + const [memoryLimit, setMemoryLimit] = useState(''); + const [memoryReserve, setMemoryReserve] = useState(''); + const [cpuShares, setCpuShares] = useState(''); + const [cpuLimit, setCpuLimit] = useState(''); + + useEffect(() => { + setMemoryLimit(String(defaults.memoryLimitMb ?? '')); + setMemoryReserve(String(defaults.memoryReserveMb ?? '')); + setCpuShares(String(defaults.cpuShares ?? '')); + setCpuLimit(String(defaults.cpuLimit ?? '')); + setEditing(false); + }, [environment.id]); + + function handleCancel() { + setMemoryLimit(String(defaults.memoryLimitMb ?? '')); + setMemoryReserve(String(defaults.memoryReserveMb ?? '')); + setCpuShares(String(defaults.cpuShares ?? '')); + setCpuLimit(String(defaults.cpuLimit ?? '')); + setEditing(false); + } + + async function handleSave() { + await onSave({ + memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, + memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, + cpuShares: cpuShares ? parseInt(cpuShares) : null, + cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, + }); + setEditing(false); + } + + return ( + <> + Default Resource Limits +

+ These defaults apply to new apps in this environment unless overridden per-app. +

+
+ Memory Limit + {editing + ? setMemoryLimit(e.target.value)} placeholder="e.g. 512" style={{ width: 100 }} /> + : {defaults.memoryLimitMb ? `${defaults.memoryLimitMb} MB` : '—'}} + + Memory Reserve + {editing + ? setMemoryReserve(e.target.value)} placeholder="e.g. 256" style={{ width: 100 }} /> + : {defaults.memoryReserveMb ? `${defaults.memoryReserveMb} MB` : '—'}} + + CPU Shares + {editing + ? setCpuShares(e.target.value)} placeholder="e.g. 512" style={{ width: 100 }} /> + : {String(defaults.cpuShares ?? '—')}} + + CPU Limit + {editing + ? setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 100 }} /> + : {defaults.cpuLimit ? `${defaults.cpuLimit} cores` : '—'}} +
+
+ {editing ? ( + <> + + + + ) : ( + + )} +
+ + ); +}