feat: add default container config editor to Environments admin page
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m20s
CI / docker (push) Successful in 1m8s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-08 18:52:39 +02:00
parent 0ccb8bc68d
commit 863a992cc4
2 changed files with 105 additions and 1 deletions

View File

@@ -54,6 +54,18 @@ export function useUpdateEnvironment() {
});
}
export function useUpdateDefaultContainerConfig() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, config }: { id: string; config: Record<string, unknown> }) =>
adminFetch<Environment>(`/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({

View File

@@ -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() {
</p>
)}
</div>
<DefaultResourcesSection environment={selected} onSave={async (config) => {
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<string, unknown>) => Promise<void>;
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 (
<>
<SectionHeader>Default Resource Limits</SectionHeader>
<p className={styles.inheritedNote}>
These defaults apply to new apps in this environment unless overridden per-app.
</p>
<div className={styles.metaGrid}>
<span className={styles.metaLabel}>Memory Limit</span>
{editing
? <Input value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} placeholder="e.g. 512" style={{ width: 100 }} />
: <span className={styles.metaValue}>{defaults.memoryLimitMb ? `${defaults.memoryLimitMb} MB` : '—'}</span>}
<span className={styles.metaLabel}>Memory Reserve</span>
{editing
? <Input value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="e.g. 256" style={{ width: 100 }} />
: <span className={styles.metaValue}>{defaults.memoryReserveMb ? `${defaults.memoryReserveMb} MB` : '—'}</span>}
<span className={styles.metaLabel}>CPU Shares</span>
{editing
? <Input value={cpuShares} onChange={(e) => setCpuShares(e.target.value)} placeholder="e.g. 512" style={{ width: 100 }} />
: <span className={styles.metaValue}>{String(defaults.cpuShares ?? '—')}</span>}
<span className={styles.metaLabel}>CPU Limit</span>
{editing
? <Input value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 100 }} />
: <span className={styles.metaValue}>{defaults.cpuLimit ? `${defaults.cpuLimit} cores` : '—'}</span>}
</div>
<div style={{ marginTop: 8, display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
{editing ? (
<>
<Button size="sm" variant="ghost" onClick={handleCancel}>Cancel</Button>
<Button size="sm" variant="primary" onClick={handleSave} loading={saving}>Save</Button>
</>
) : (
<Button size="sm" variant="secondary" onClick={() => setEditing(true)}>Edit Defaults</Button>
)}
</div>
</>
);
}