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) <noreply@anthropic.com>
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user