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() {
|
export function useDeleteEnvironment() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
useCreateEnvironment,
|
useCreateEnvironment,
|
||||||
useUpdateEnvironment,
|
useUpdateEnvironment,
|
||||||
useDeleteEnvironment,
|
useDeleteEnvironment,
|
||||||
|
useUpdateDefaultContainerConfig,
|
||||||
} from '../../api/queries/admin/environments';
|
} from '../../api/queries/admin/environments';
|
||||||
import type { Environment } from '../../api/queries/admin/environments';
|
import type { Environment } from '../../api/queries/admin/environments';
|
||||||
import styles from './UserManagement.module.css';
|
import styles from './UserManagement.module.css';
|
||||||
@@ -42,6 +43,7 @@ export default function EnvironmentsPage() {
|
|||||||
const createEnv = useCreateEnvironment();
|
const createEnv = useCreateEnvironment();
|
||||||
const updateEnv = useUpdateEnvironment();
|
const updateEnv = useUpdateEnvironment();
|
||||||
const deleteEnv = useDeleteEnvironment();
|
const deleteEnv = useDeleteEnvironment();
|
||||||
|
const updateDefaults = useUpdateDefaultContainerConfig();
|
||||||
|
|
||||||
const selected = useMemo(
|
const selected = useMemo(
|
||||||
() => environments.find((e) => e.id === selectedId) ?? null,
|
() => environments.find((e) => e.id === selectedId) ?? null,
|
||||||
@@ -280,6 +282,15 @@ export default function EnvironmentsPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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
|
) : 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