/** * @file UserQuota.tsx * @description Spend quotas per user on the Users page (server mode; * server/lib/user-quota.js): `QuotaSummary`, a row's "spent / limit per day" * line with its bar — red once used up, with when it opens again; * `QuotaFields`, a limit in dollars and its period (a day from local midnight, * or a sliding 5 hours); and `QuotaDefaultCard`, the default every user * without one of their own gets — an admin edits it with their own password * again (`PUT /api/users/quota-default`), an analyst only reads. A user's own * setting is part of the Edit user form (`UserEditDialog`). A user over their * quota can start no session and their prompts are refused until it resets. * @author Nguyễn Ngọc Trí Vĩ */ import { useState, type KeyboardEvent } from "react"; import { useTranslation } from "react-i18next"; import { Gauge, Pencil } from "lucide-react"; import { api, type UserQuota, type UserQuotaStatus } from "../../lib/api"; import { fmtCost, formatDateTime } from "../../lib/format"; import { isSubmitEnter } from "../../lib/keys"; import { Checkbox } from "../Checkbox"; import { ConfirmModal } from "../ConfirmModal"; import { Meter } from "../health/HostHealth"; export type QuotaPeriod = UserQuota["period"]; export const QUOTA_PERIODS: QuotaPeriod[] = ["day", "5h"]; /** "$10 / day", "$2.50 / 5 h". */ export function useQuotaLabel() { const { t } = useTranslation("users"); return (q: Pick) => t(`quota.per.${q.period}`, { limit: fmtCost(q.limit_usd) }); } /** A row's quota: spent of the limit, a bar, and when a used-up one opens again. */ export function QuotaSummary({ quota }: { quota: UserQuotaStatus | null | undefined }) { const { t } = useTranslation("users"); const label = useQuotaLabel(); if (quota === undefined) return null; if (quota === null) { return
{t("quota.none")}
; } return (
{t("quota.used", { spend: fmtCost(quota.spend), limit: label(quota) })} {quota.source === "default" && {t("quota.defaultTag")}}
); } /** A dollar limit and its period. `limit` is the raw input text. */ export function QuotaFields({ limit, period, onLimit, onPeriod, onKeyDown, disabled, }: { limit: string; period: QuotaPeriod; onLimit: (v: string) => void; onPeriod: (v: QuotaPeriod) => void; onKeyDown?: (e: KeyboardEvent) => void; disabled?: boolean; }) { const { t } = useTranslation("users"); return (
$ onLimit(e.target.value)} onKeyDown={onKeyDown} aria-label={t("quota.limit")} />
); } /** A positive dollar amount from the input text, or null. */ export function parseLimit(text: string): number | null { const n = Number(text); return text.trim() !== "" && Number.isFinite(n) && n > 0 ? Math.round(n * 100) / 100 : null; } /** The default quota: shown to staff, edited by an admin with their own password. */ export function QuotaDefaultCard({ quotaDefault, isAdmin, onSaved, }: { /** `undefined` while loading, null for none. */ quotaDefault: UserQuota | null | undefined; isAdmin: boolean; onSaved: (message: string) => void; }) { const { t } = useTranslation("users"); const label = useQuotaLabel(); const [editing, setEditing] = useState(false); return (

{t("quota.cardTitle")}

{t("quota.cardDescription")}

{quotaDefault === undefined ? "…" : quotaDefault ? label(quotaDefault) : t("quota.noDefault")}
{isAdmin && quotaDefault !== undefined && ( )}
{editing && ( setEditing(false)} onDone={(message) => { setEditing(false); onSaved(message); }} /> )}
); } function QuotaDefaultDialog({ current, onClose, onDone, }: { current: UserQuota | null; onClose: () => void; onDone: (message: string) => void; }) { const { t } = useTranslation("users"); const [on, setOn] = useState(current !== null); const [limit, setLimit] = useState(current ? String(current.limit_usd) : ""); const [period, setPeriod] = useState(current?.period ?? "day"); const [password, setPassword] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const value = parseLimit(limit); const ready = password.length > 0 && (!on || value !== null) && !busy; const submit = async () => { if (!ready) return; setBusy(true); setError(null); try { await api.users.setQuotaDefault( on && value !== null ? { limit_usd: value, period, tz_offset: new Date().getTimezoneOffset() } : null, password ); onDone(t("quota.defaultSaved")); } catch (e) { const err = e as Error & { code?: string }; setError( err.code && t(`errors.${err.code}`, { defaultValue: "" }) ? t(`errors.${err.code}`) : err.message ); } finally { setBusy(false); } }; const onKeyDown = (e: KeyboardEvent) => { if (isSubmitEnter(e)) { e.preventDefault(); void submit(); } }; return ( void submit()} onCancel={onClose} >

{t("quota.enforceHint")}

{error && (

{error}

)}
); }