diff --git a/messages/en-US.json b/messages/en-US.json index f13489f55..363a2a99d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1099,7 +1099,7 @@ "accessRoleErrorNewRequired": "New role is required", "accessRoleErrorRemove": "Failed to remove role", "accessRoleErrorRemoveDescription": "An error occurred while removing the role.", - "accessRoleInferenceBudget": "Inference Budget", + "accessRoleInferenceBudget": "AI Budget", "accessRoleInferenceBudgetDescription": "Configure how members of this role restrict AI usage based on spending or token limits", "accessRoleName": "Role Name", "accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.", @@ -1720,7 +1720,7 @@ "virtualApiKeysErrorFetchSecret": "Error loading secret", "virtualApiKeysErrorFetchSecretDescription": "Failed to load the virtual API key secret", "virtualApiKeysFilterUnassigned": "Unassigned", - "virtualApiKeysInferenceBudget": "Inference Budget", + "virtualApiKeysInferenceBudget": "Budget", "virtualApiKeysInferenceBudgetDescription": "Configure how this key restricts AI usage based on spending or token limits", "myVirtualApiKeysTitle": "Your API Keys", "myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization", @@ -1924,10 +1924,10 @@ "aiProviderModelsSourceAll": "Matches every model key", "aiProviderModelsBudgetConfigured": "Budget configured", "aiProviderModelsEditTitle": "Edit Model", - "aiProviderModelsEditDescription": "Update the model key or configure its inference budget.", - "aiProviderModelsBudgetTab": "Inference Budget", + "aiProviderModelsEditDescription": "Update the model key or configure its budget.", + "aiProviderModelsBudgetTab": "Budget", "aiProviderModelsBudgetDescription": "Configure how this model restricts usage based on spending or token limits", - "aiProviderModelsBudgetUnsaved": "Save this model first to configure its inference budget.", + "aiProviderModelsBudgetUnsaved": "Save this model first to configure its budget.", "aiProviderModelsKeyLabel": "Model Key", "aiProviderModelsKeyRequired": "Enter a model key", "aiProviderModelsKeyDuplicate": "This model key is already on a list", diff --git a/src/components/AiProviderModelListEditor.tsx b/src/components/AiProviderModelListEditor.tsx index 38014d25f..4da6fb10c 100644 --- a/src/components/AiProviderModelListEditor.tsx +++ b/src/components/AiProviderModelListEditor.tsx @@ -964,8 +964,7 @@ function EditModelCredenza({ rows={pendingBudgetRows} onChange={setPendingBudgetRows} disabled={ - budgetsQuery.isLoading || - savingBudgets + budgetsQuery.isLoading } attemptedSave={ attemptedBudgetsSave diff --git a/src/components/BudgetsEditor.tsx b/src/components/BudgetsEditor.tsx index 3ccbfad3c..fe4cf2f9a 100644 --- a/src/components/BudgetsEditor.tsx +++ b/src/components/BudgetsEditor.tsx @@ -1,6 +1,8 @@ "use client"; import { + SettingsFormCell, + SettingsFormGrid, SettingsSection, SettingsSectionBody, SettingsSectionDescription, @@ -9,7 +11,6 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { Button } from "@app/components/ui/button"; -import { DataTableEmptyState } from "@app/components/ui/data-table-empty-state"; import { Input } from "@app/components/ui/input"; import { Select, @@ -18,14 +19,6 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@app/components/ui/table"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -37,6 +30,7 @@ import { type AiBudgetScope, type AiBudgetUnit } from "@app/lib/aiBudgetScope"; +import { cn } from "@app/lib/cn"; import { aiBudgetQueries } from "@app/lib/queries"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import type { AiBudget } from "@server/db"; @@ -127,6 +121,113 @@ export function getBudgetRowsErrors(rows: BudgetRow[]): { return { conflictingKeys, invalidAmountKeys }; } +type BudgetRowFieldProps = { + row: BudgetRow; + disabled: boolean; + showInvalidAmount: boolean; + showConflict: boolean; + unitLabels: Record; + periodLabels: Record; + amountPlaceholder: string; + onUpdate: (patch: Partial) => void; +}; + +function BudgetRowAmountInput({ + row, + disabled, + showInvalidAmount, + amountPlaceholder, + onUpdate, + className +}: Pick< + BudgetRowFieldProps, + "row" | "disabled" | "showInvalidAmount" | "amountPlaceholder" | "onUpdate" +> & { className?: string }) { + return ( + onUpdate({ amount: e.target.value })} + className={cn("w-full min-w-0", className)} + /> + ); +} + +function BudgetRowUnitSelect({ + row, + disabled, + showConflict, + unitLabels, + onUpdate, + className +}: Pick< + BudgetRowFieldProps, + "row" | "disabled" | "showConflict" | "unitLabels" | "onUpdate" +> & { className?: string }) { + return ( + + ); +} + +function BudgetRowPeriodSelect({ + row, + disabled, + showConflict, + periodLabels, + onUpdate, + className +}: Pick< + BudgetRowFieldProps, + "row" | "disabled" | "showConflict" | "periodLabels" | "onUpdate" +> & { className?: string }) { + return ( + + ); +} + export function BudgetRowsFields({ rows, onChange, @@ -173,6 +274,8 @@ export function BudgetRowsFields({ tokens: t("aiBudgetUnitTokens") }; + const amountPlaceholder = t("aiBudgetAmountPlaceholder"); + const addRowButton = ( + + ); + })} + )} + {errorMessage} {rows.length > 0 && addRowButton} ); @@ -474,12 +519,16 @@ export function BudgetsEditor({ const body = ( <> - + + + + + diff --git a/src/components/CreateVirtualApiKeyForm.tsx b/src/components/CreateVirtualApiKeyForm.tsx index c443ad4b9..4a97d358a 100644 --- a/src/components/CreateVirtualApiKeyForm.tsx +++ b/src/components/CreateVirtualApiKeyForm.tsx @@ -99,9 +99,7 @@ export default function CreateVirtualApiKeyForm({ const [selectedResources, setSelectedResources] = useState< SelectedResource[] >([]); - const [pendingBudgetRows, setPendingBudgetRows] = useState( - [] - ); + const [pendingBudgetRows, setPendingBudgetRows] = useState([]); const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false); const formSchema = z.object({ @@ -204,10 +202,7 @@ export default function CreateVirtualApiKeyForm({ toast({ variant: "destructive", title: t("aiBudgetErrorSave"), - description: formatAxiosError( - e, - t("aiBudgetErrorSave") - ) + description: formatAxiosError(e, t("aiBudgetErrorSave")) }); } } @@ -295,120 +290,47 @@ export default function CreateVirtualApiKeyForm({ } ]} > -
- ( - - - {t("virtualApiKeysName")} - - - - - - - )} - /> - - ( - - - {t( - "virtualApiKeysDescriptionOptional" - )} - - - - - - - )} - /> - -
- - {t( - "virtualApiKeysAssociateUserOptional" - )} - - - - - - - - - -

- {t( - "virtualApiKeysAssociateUserDescription" - )} -

-
- -
-
- { - setAllResources( - val as boolean - ); - if (val) { - setSelectedResources( - [] - ); - } - }} - className="mt-0.5" +
+ ( + + + {t( + "virtualApiKeysName" + )} + + + + + + + )} + /> + + ( + + + {t( + "virtualApiKeysDescriptionOptional" + )} + + + + + + + )} /> -
- -

- {t( - "virtualApiKeysAllResourcesDescription" - )} -

-
-
- {!allResources && (
{t( - "virtualApiKeysSelectResources" + "virtualApiKeysAssociateUserOptional" )} @@ -418,69 +340,155 @@ export default function CreateVirtualApiKeyForm({ role="combobox" className={cn( "w-full justify-between", - selectedResources.length === - 0 && + !selectedUser && "text-muted-foreground" )} > - - {formatMultiResourcesSelectorLabel( - selectedResources, - t, - "virtualApiKeysSelectResourcesPlaceholder" - )} - + {selectedUser?.text + ? selectedUser.text + : t( + "userSelect" + )} - - + - 0 - } - onClear={() => - setSelectedResources( - [] - ) + onSelectUser={ + setSelectedUser } /> - +

{t( - "virtualApiKeysSelectResourcesDescription" + "virtualApiKeysAssociateUserDescription" )} - +

- )} -
-
-
-

- {t( - "virtualApiKeysInferenceBudgetDescription" - )} -

- -
+
+
+ { + setAllResources( + val as boolean + ); + if (val) { + setSelectedResources( + [] + ); + } + }} + className="mt-0.5" + /> +
+ +

+ {t( + "virtualApiKeysAllResourcesDescription" + )} +

+
+
+ + {!allResources && ( +
+ + {t( + "virtualApiKeysSelectResources" + )} + + + + + + + + 0 + } + onClear={() => + setSelectedResources( + [] + ) + } + /> + + + + {t( + "virtualApiKeysSelectResourcesDescription" + )} + +
+ )} +
+
+ +
+

+ {t( + "virtualApiKeysInferenceBudgetDescription" + )} +

+ +
diff --git a/src/components/EditVirtualApiKeyForm.tsx b/src/components/EditVirtualApiKeyForm.tsx index 1d3e78dbd..b8fbcac98 100644 --- a/src/components/EditVirtualApiKeyForm.tsx +++ b/src/components/EditVirtualApiKeyForm.tsx @@ -112,9 +112,7 @@ export default function EditVirtualApiKeyForm({ >([]); const [credential, setCredential] = useState(null); const [credentialLoading, setCredentialLoading] = useState(false); - const [pendingBudgetRows, setPendingBudgetRows] = useState( - [] - ); + const [pendingBudgetRows, setPendingBudgetRows] = useState([]); const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false); const budgetScope = { @@ -350,9 +348,7 @@ export default function EditVirtualApiKeyForm({
@@ -369,99 +365,11 @@ export default function EditVirtualApiKeyForm({ } ]} > -
-
- - - - - - - - - -

- {t( - "virtualApiKeysAssociateUserDescription" - )} -

-
- -
- ( - -
- - { - field.onChange( - val as boolean - ); - if (val) { - setSelectedResources( - [] - ); - } - }} - className="mt-0.5" - /> - -
- -

- {t( - "virtualApiKeysAllResourcesDescription" - )} -

-
-
- -
- )} - /> - - {!allResources && ( +
@@ -471,66 +379,163 @@ export default function EditVirtualApiKeyForm({ role="combobox" className={cn( "w-full justify-between", - selectedResources.length === - 0 && + !selectedUser && "text-muted-foreground" )} > - - {formatMultiResourcesSelectorLabel( - selectedResources, - t, - "virtualApiKeysSelectResourcesPlaceholder" - )} - + {selectedUser?.text + ? selectedUser.text + : t("userSelect")} - - + - 0 - } - onClear={() => - setSelectedResources( - [] - ) + onSelectUser={ + setSelectedUser } /> - +

{t( - "virtualApiKeysSelectResourcesRequired" + "virtualApiKeysAssociateUserDescription" )} - +

- )} -
-
-
-

- {t( - "virtualApiKeysInferenceBudgetDescription" - )} -

- -
+
+ ( + +
+ + { + field.onChange( + val as boolean + ); + if ( + val + ) { + setSelectedResources( + [] + ); + } + }} + className="mt-0.5" + /> + +
+ +

+ {t( + "virtualApiKeysAllResourcesDescription" + )} +

+
+
+ +
+ )} + /> + + {!allResources && ( +
+ + + + + + + + 0 + } + onClear={() => + setSelectedResources( + [] + ) + } + /> + + + + {t( + "virtualApiKeysSelectResourcesRequired" + )} + +
+ )} +
+
+ +
+

+ {t( + "virtualApiKeysInferenceBudgetDescription" + )} +

+ +
diff --git a/src/components/RoleForm.tsx b/src/components/RoleForm.tsx index 4e8f457cb..186da2a00 100644 --- a/src/components/RoleForm.tsx +++ b/src/components/RoleForm.tsx @@ -219,9 +219,7 @@ export function RoleForm({ useState(null); const [dragOverField, setDragOverField] = useState(null); - const [pendingBudgetRows, setPendingBudgetRows] = useState( - [] - ); + const [pendingBudgetRows, setPendingBudgetRows] = useState([]); const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false); const budgetsQuery = useQuery({ @@ -702,7 +700,6 @@ export function RoleForm({
)} - {/* Inference Budget tab */}

{t("accessRoleInferenceBudgetDescription")} @@ -711,8 +708,7 @@ export function RoleForm({ rows={pendingBudgetRows} onChange={setPendingBudgetRows} disabled={ - variant === "edit" && - budgetsQuery.isLoading + variant === "edit" && budgetsQuery.isLoading } attemptedSave={attemptedBudgetsSave} />