From 863a992cc4711bd7b911bd83aa42d3cd6110cf06 Mon Sep 17 00:00:00 2001
From: hsiegeln <37154749+hsiegeln@users.noreply.github.com>
Date: Wed, 8 Apr 2026 18:52:39 +0200
Subject: [PATCH] feat: add default container config editor to Environments
admin page
New "Default Resource Limits" section in environment detail view with
memory limit/reserve, CPU shares/limit. These defaults apply to new
apps unless overridden per-app.
Added useUpdateDefaultContainerConfig hook for the PUT endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
ui/src/api/queries/admin/environments.ts | 12 +++
ui/src/pages/Admin/EnvironmentsPage.tsx | 94 +++++++++++++++++++++++-
2 files changed, 105 insertions(+), 1 deletion(-)
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 ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+ >
+ );
+}