mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
refactor(ui): migrate cost-tracking and users off antd and Tremor
Replaces antd and Tremor markup in the four route-exclusive components with installed shadcn primitives from @/components/ui plus token utilities. Markup only; no data flow, request or props contract changes. The route tests were not touched by this commit and stay green, which is what makes them evidence: they were written against the antd/Tremor markup.
This commit is contained in:
parent
ec0fe36974
commit
fdafcf28b2
5 changed files with 298 additions and 255 deletions
|
|
@ -260,9 +260,6 @@
|
|||
"src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/index.tsx": {
|
||||
|
|
@ -294,9 +291,6 @@
|
|||
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/use_multi_cost_estimate.ts": {
|
||||
|
|
@ -1860,11 +1854,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/users/_components/DefaultUserSettings.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/users/_components/edit_user.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
@ -1901,9 +1890,6 @@
|
|||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 3
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 1
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { Text, TextInput } from "@tremor/react";
|
||||
import CodeBlock from "@/components/CodeBlock";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const HowItWorks: React.FC = () => {
|
||||
const [responseCost, setResponseCost] = useState("");
|
||||
|
|
@ -28,30 +28,30 @@ const HowItWorks: React.FC = () => {
|
|||
return (
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<Text className="font-medium text-gray-900 text-sm mb-1">Cost Calculation</Text>
|
||||
<Text className="text-xs text-gray-600">
|
||||
<p className="mb-1 text-sm font-medium text-foreground">Cost Calculation</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Discounts are applied to provider costs:{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded-sm text-xs">
|
||||
<code className="rounded-sm bg-muted px-1.5 py-0.5 text-xs">
|
||||
final_cost = base_cost × (1 - discount%/100)
|
||||
</code>
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium text-gray-900 text-sm mb-1">Example</Text>
|
||||
<Text className="text-xs text-gray-600">
|
||||
<p className="mb-1 text-sm font-medium text-foreground">Example</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
A 5% discount on a $10.00 request results in: $10.00 × (1 - 0.05) = $9.50
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium text-gray-900 text-sm mb-1">Valid Range</Text>
|
||||
<Text className="text-xs text-gray-600">Discount percentages must be between 0% and 100%</Text>
|
||||
<p className="mb-1 text-sm font-medium text-foreground">Valid Range</p>
|
||||
<p className="text-xs text-muted-foreground">Discount percentages must be between 0% and 100%</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<Text className="font-medium text-gray-900 text-sm mb-2">Validating Discounts</Text>
|
||||
<Text className="text-xs text-gray-600 mb-3">
|
||||
<div className="border-t border-border pt-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Validating Discounts</p>
|
||||
<p className="mb-3 text-xs text-muted-foreground">
|
||||
Make a test request and check the response headers to verify discounts are applied:
|
||||
</Text>
|
||||
</p>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
code={`curl -X POST -i http://your-proxy:4000/chat/completions \\
|
||||
|
|
@ -62,78 +62,80 @@ const HowItWorks: React.FC = () => {
|
|||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
}'`}
|
||||
/>
|
||||
<Text className="text-xs text-gray-600 mt-3 mb-2">Look for these headers in the response:</Text>
|
||||
<p className="mt-3 mb-2 text-xs text-muted-foreground">Look for these headers in the response:</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-start gap-3">
|
||||
<code className="bg-gray-100 px-2 py-1 rounded-sm text-xs font-mono text-gray-800 whitespace-nowrap">
|
||||
<code className="rounded-sm bg-muted px-2 py-1 font-mono text-xs whitespace-nowrap">
|
||||
x-litellm-response-cost
|
||||
</code>
|
||||
<Text className="text-xs text-gray-600">Final cost after discount</Text>
|
||||
<p className="text-xs text-muted-foreground">Final cost after discount</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<code className="bg-gray-100 px-2 py-1 rounded-sm text-xs font-mono text-gray-800 whitespace-nowrap">
|
||||
<code className="rounded-sm bg-muted px-2 py-1 font-mono text-xs whitespace-nowrap">
|
||||
x-litellm-response-cost-original
|
||||
</code>
|
||||
<Text className="text-xs text-gray-600">Original cost before discount</Text>
|
||||
<p className="text-xs text-muted-foreground">Original cost before discount</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<code className="bg-gray-100 px-2 py-1 rounded-sm text-xs font-mono text-gray-800 whitespace-nowrap">
|
||||
<code className="rounded-sm bg-muted px-2 py-1 font-mono text-xs whitespace-nowrap">
|
||||
x-litellm-response-cost-discount-amount
|
||||
</code>
|
||||
<Text className="text-xs text-gray-600">Amount discounted</Text>
|
||||
<p className="text-xs text-muted-foreground">Amount discounted</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<Text className="font-medium text-gray-900 text-sm mb-3">Discount Calculator</Text>
|
||||
<Text className="text-xs text-gray-600 mb-3">
|
||||
<div className="border-t border-border pt-4">
|
||||
<p className="mb-3 text-sm font-medium text-foreground">Discount Calculator</p>
|
||||
<p className="mb-3 text-xs text-muted-foreground">
|
||||
Enter values from your response headers to verify the discount:
|
||||
</Text>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
</p>
|
||||
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
<label className="mb-1 block text-xs font-medium text-foreground" htmlFor="how-it-works-response-cost">
|
||||
Response Cost (x-litellm-response-cost)
|
||||
</label>
|
||||
<TextInput
|
||||
<Input
|
||||
id="how-it-works-response-cost"
|
||||
placeholder="0.0171938125"
|
||||
value={responseCost}
|
||||
onValueChange={setResponseCost}
|
||||
onChange={(event) => setResponseCost(event.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
<label className="mb-1 block text-xs font-medium text-foreground" htmlFor="how-it-works-discount-amount">
|
||||
Discount Amount (x-litellm-response-cost-discount-amount)
|
||||
</label>
|
||||
<TextInput
|
||||
<Input
|
||||
id="how-it-works-discount-amount"
|
||||
placeholder="0.0009049375"
|
||||
value={discountAmount}
|
||||
onValueChange={setDiscountAmount}
|
||||
onChange={(event) => setDiscountAmount(event.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{calculatedDiscount && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<Text className="text-sm font-medium text-blue-900 mb-2">Calculated Results</Text>
|
||||
<div className="rounded-lg border border-border bg-muted p-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Calculated Results</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className="text-xs text-blue-800">Original Cost:</Text>
|
||||
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.originalCost}</code>
|
||||
<p className="text-xs text-muted-foreground">Original Cost:</p>
|
||||
<code className="font-mono text-xs text-foreground">${calculatedDiscount.originalCost}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className="text-xs text-blue-800">Final Cost:</Text>
|
||||
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.finalCost}</code>
|
||||
<p className="text-xs text-muted-foreground">Final Cost:</p>
|
||||
<code className="font-mono text-xs text-foreground">${calculatedDiscount.finalCost}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className="text-xs text-blue-800">Discount Amount:</Text>
|
||||
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.discountAmount}</code>
|
||||
<p className="text-xs text-muted-foreground">Discount Amount:</p>
|
||||
<code className="font-mono text-xs text-foreground">${calculatedDiscount.discountAmount}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 border-t border-blue-300">
|
||||
<Text className="text-xs font-semibold text-blue-900">Discount Applied:</Text>
|
||||
<Text className="text-sm font-bold text-blue-900">{calculatedDiscount.discountPercentage}%</Text>
|
||||
<div className="flex items-center justify-between border-t border-border pt-2">
|
||||
<p className="text-xs font-semibold text-foreground">Discount Applied:</p>
|
||||
<p className="text-sm font-bold text-foreground">{calculatedDiscount.discountPercentage}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "@tremor/react";
|
||||
import { DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons";
|
||||
import { Download, FileSpreadsheet, FileText } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MultiModelResult } from "./types";
|
||||
import { exportMultiToPDF, exportMultiToCSV } from "./multi_export_utils";
|
||||
|
||||
|
|
@ -36,30 +36,31 @@ const MultiExportDropdown: React.FC<MultiExportDropdownProps> = ({ multiResult }
|
|||
|
||||
return (
|
||||
<div className="relative inline-block" ref={menuRef}>
|
||||
<Button size="xs" variant="secondary" icon={DownloadOutlined} onClick={() => setIsOpen(!isOpen)}>
|
||||
<Button size="xs" variant="secondary" onClick={() => setIsOpen(!isOpen)}>
|
||||
<Download />
|
||||
Export
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
||||
<div className="absolute right-0 z-50 mt-1 w-44 rounded-lg border border-border bg-popover py-1 shadow-lg">
|
||||
<button
|
||||
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
className="flex w-full items-center px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted"
|
||||
onClick={() => {
|
||||
exportMultiToPDF(multiResult);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<FilePdfOutlined className="mr-3 text-red-500" />
|
||||
<FileText className="mr-3 size-4" />
|
||||
Export as PDF
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
className="flex w-full items-center px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted"
|
||||
onClick={() => {
|
||||
exportMultiToCSV(multiResult);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<FileExcelOutlined className="mr-3 text-green-600" />
|
||||
<FileSpreadsheet className="mr-3 size-4" />
|
||||
Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Title, Text, Divider, TextInput } from "@tremor/react";
|
||||
import { Button, Typography, Spin, Switch, Select, InputNumber } from "antd";
|
||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { getInternalUserSettings, updateInternalUserSettings, modelAvailableCall } from "@/components/networking";
|
||||
import BudgetDurationDropdown, {
|
||||
getBudgetDurationLabel,
|
||||
|
|
@ -9,6 +7,25 @@ import BudgetDurationDropdown, {
|
|||
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import NotificationManager from "@/components/molecules/notifications_manager";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
} from "@/components/ui/combobox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
|
||||
interface DefaultUserSettingsProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -23,6 +40,13 @@ interface TeamEntry {
|
|||
user_role: "user" | "admin";
|
||||
}
|
||||
|
||||
const TEAM_MEMBER_ROLES: TeamEntry["user_role"][] = ["user", "admin"];
|
||||
|
||||
const TEAM_MEMBER_ROLE_LABELS: Record<TeamEntry["user_role"], string> = {
|
||||
user: "User",
|
||||
admin: "Admin",
|
||||
};
|
||||
|
||||
const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
||||
accessToken,
|
||||
possibleUIRoles,
|
||||
|
|
@ -35,8 +59,6 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
const [editedValues, setEditedValues] = useState<any>({});
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSSOSettings = async () => {
|
||||
|
|
@ -129,16 +151,48 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const renderMultiSelect = (key: string, options: string[], placeholder: string, getLabel: (o: string) => string) => {
|
||||
const selected: string[] = editedValues[key] || [];
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
multiple
|
||||
items={options}
|
||||
value={selected}
|
||||
onValueChange={(value: string[]) => handleTextInputChange(key, value)}
|
||||
>
|
||||
<ComboboxChips className="mt-2">
|
||||
<ComboboxValue>
|
||||
{(values: string[]) =>
|
||||
values.map((option) => (
|
||||
<ComboboxChip key={option} aria-label={getLabel(option)}>
|
||||
{getLabel(option)}
|
||||
</ComboboxChip>
|
||||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput placeholder={placeholder} className="border-0 bg-transparent" />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No options found</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(option: string) => (
|
||||
<ComboboxItem key={option} value={option}>
|
||||
{getLabel(option)}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
// Teams editor component
|
||||
const renderTeamsEditor = (teams: any[]) => {
|
||||
const normalizedTeams = normalizeTeams(teams);
|
||||
|
||||
const updateTeam = (index: number, field: keyof TeamEntry, value: any) => {
|
||||
const updatedTeams = [...normalizedTeams];
|
||||
updatedTeams[index] = {
|
||||
...updatedTeams[index],
|
||||
[field]: value,
|
||||
};
|
||||
const updatedTeams = normalizedTeams.map((team, i) => (i === index ? { ...team, [field]: value } : team));
|
||||
handleTextInputChange("teams", updatedTeams);
|
||||
};
|
||||
|
||||
|
|
@ -158,53 +212,69 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
return (
|
||||
<div className="space-y-3">
|
||||
{normalizedTeams.map((team, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Text className="font-medium">Team {index + 1}</Text>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => removeTeam(index)}>
|
||||
<div key={index} className="rounded-lg border border-border bg-muted/40 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="font-medium">Team {index + 1}</p>
|
||||
<Button size="sm" variant="destructive" onClick={() => removeTeam(index)}>
|
||||
<Trash2 />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">Team ID</Text>
|
||||
<TextInput
|
||||
<p className="mb-1 text-sm font-medium">Team ID</p>
|
||||
<Input
|
||||
value={team.team_id}
|
||||
onChange={(e) => updateTeam(index, "team_id", e.target.value)}
|
||||
onChange={(event) => updateTeam(index, "team_id", event.target.value)}
|
||||
placeholder="Enter team ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">Max Budget in Team</Text>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
value={team.max_budget_in_team}
|
||||
onChange={(value) => updateTeam(index, "max_budget_in_team", value)}
|
||||
<p className="mb-1 text-sm font-medium">Max Budget in Team</p>
|
||||
<Input
|
||||
type="number"
|
||||
value={team.max_budget_in_team ?? ""}
|
||||
onChange={(event) =>
|
||||
updateTeam(
|
||||
index,
|
||||
"max_budget_in_team",
|
||||
event.target.value === "" ? undefined : Number(event.target.value),
|
||||
)
|
||||
}
|
||||
placeholder="Optional"
|
||||
min={0}
|
||||
step={0.01}
|
||||
precision={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">User Role</Text>
|
||||
<p className="mb-1 text-sm font-medium">User Role</p>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
value={team.user_role}
|
||||
onChange={(value) => updateTeam(index, "user_role", value)}
|
||||
onValueChange={(value: TeamEntry["user_role"] | null) =>
|
||||
updateTeam(index, "user_role", value ?? "user")
|
||||
}
|
||||
>
|
||||
<Option value="user">User</Option>
|
||||
<Option value="admin">Admin</Option>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue>{TEAM_MEMBER_ROLE_LABELS[team.user_role]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TEAM_MEMBER_ROLES.map((role) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
{TEAM_MEMBER_ROLE_LABELS[role]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button icon={<PlusOutlined />} onClick={addTeam} className="w-full">
|
||||
<Button variant="outline" onClick={addTeam} className="w-full">
|
||||
<Plus />
|
||||
Add Team
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -217,23 +287,24 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
if (key === "teams") {
|
||||
return <div className="mt-2">{renderTeamsEditor(editedValues[key] || [])}</div>;
|
||||
} else if (key === "user_role" && possibleUIRoles) {
|
||||
const internalUserRoles = Object.entries(possibleUIRoles).filter(([role]) => role.includes("internal_user"));
|
||||
const selectedRole = editedValues[key] || null;
|
||||
|
||||
return (
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || ""}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{Object.entries(possibleUIRoles)
|
||||
.filter(([role]) => role.includes("internal_user"))
|
||||
.map(([role, { ui_label, description }]) => (
|
||||
<Option key={role} value={role}>
|
||||
<div className="flex items-center">
|
||||
<span>{ui_label}</span>
|
||||
<span className="ml-2 text-xs text-gray-500">{description}</span>
|
||||
</div>
|
||||
</Option>
|
||||
<Select value={selectedRole} onValueChange={(role: string) => handleTextInputChange(key, role)}>
|
||||
<SelectTrigger className="mt-2 w-full">
|
||||
<SelectValue>
|
||||
{selectedRole ? possibleUIRoles[selectedRole]?.ui_label || selectedRole : "Select a role"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{internalUserRoles.map(([role, { ui_label, description }]) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
<span>{ui_label}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">{description}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else if (key === "budget_duration") {
|
||||
|
|
@ -247,67 +318,47 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
} else if (type === "boolean") {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Switch checked={!!editedValues[key]} onChange={(checked) => handleTextInputChange(key, checked)} />
|
||||
<Switch
|
||||
checked={!!editedValues[key]}
|
||||
onCheckedChange={(checked: boolean) => handleTextInputChange(key, checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (type === "array" && property.items?.enum) {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.items.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
return renderMultiSelect(key, property.items.enum as string[], "Select options", (option) => option);
|
||||
} else if (key === "models") {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
<Option key="no-default-models" value="no-default-models">
|
||||
No Default Models
|
||||
</Option>
|
||||
<Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
</Option>
|
||||
{availableModels.map((model: string) => (
|
||||
<Option key={model} value={model}>
|
||||
{getModelDisplayName(model)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
return renderMultiSelect(
|
||||
key,
|
||||
["no-default-models", "all-proxy-models", ...availableModels],
|
||||
"Select models",
|
||||
(model) => {
|
||||
if (model === "no-default-models") return "No Default Models";
|
||||
if (model === "all-proxy-models") return "All Proxy Models";
|
||||
return getModelDisplayName(model);
|
||||
},
|
||||
);
|
||||
} else if (type === "string" && property.enum) {
|
||||
const selected = editedValues[key] || null;
|
||||
|
||||
return (
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || ""}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
<Select value={selected} onValueChange={(option: string) => handleTextInputChange(key, option)}>
|
||||
<SelectTrigger className="mt-2 w-full">
|
||||
<SelectValue placeholder={`Select ${key.replace(/_/g, " ")}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(property.enum as string[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TextInput
|
||||
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
|
||||
onChange={(e) => handleTextInputChange(key, e.target.value)}
|
||||
<Input
|
||||
value={editedValues[key] !== undefined && editedValues[key] !== null ? String(editedValues[key]) : ""}
|
||||
onChange={(event) => handleTextInputChange(key, event.target.value)}
|
||||
placeholder={property.description || ""}
|
||||
className="mt-2"
|
||||
/>
|
||||
|
|
@ -316,33 +367,33 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
};
|
||||
|
||||
const renderValue = (key: string, value: any): JSX.Element => {
|
||||
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||
if (value === null || value === undefined) return <span className="text-muted-foreground">Not set</span>;
|
||||
|
||||
if (key === "teams" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">No teams assigned</span>;
|
||||
if (value.length === 0) return <span className="text-muted-foreground">No teams assigned</span>;
|
||||
|
||||
const normalizedTeams = normalizeTeams(value);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-1">
|
||||
<div className="mt-1 space-y-2">
|
||||
{normalizedTeams.map((team, index) => (
|
||||
<div key={index} className="border rounded-lg p-3 bg-white">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
|
||||
<div key={index} className="rounded-lg border border-border bg-card p-3">
|
||||
<div className="grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Team ID:</span>
|
||||
<p className="text-gray-900">{team.team_id || "Not specified"}</p>
|
||||
<span className="font-medium text-muted-foreground">Team ID:</span>
|
||||
<p>{team.team_id || "Not specified"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Max Budget:</span>
|
||||
<p className="text-gray-900">
|
||||
<span className="font-medium text-muted-foreground">Max Budget:</span>
|
||||
<p>
|
||||
{team.max_budget_in_team !== undefined
|
||||
? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}`
|
||||
: "No limit"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Role:</span>
|
||||
<p className="text-gray-900 capitalize">{team.user_role}</p>
|
||||
<span className="font-medium text-muted-foreground">Role:</span>
|
||||
<p className="capitalize">{team.user_role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -356,7 +407,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
return (
|
||||
<div>
|
||||
<span className="font-medium">{ui_label}</span>
|
||||
{description && <p className="text-xs text-gray-500 mt-1">{description}</p>}
|
||||
{description && <p className="mt-1 text-xs text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -370,14 +421,14 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
}
|
||||
|
||||
if (key === "models" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
if (value.length === 0) return <span className="text-muted-foreground">None</span>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{value.map((model, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-100 rounded-sm text-xs">
|
||||
<Badge key={index} variant="secondary">
|
||||
{getModelDisplayName(model)}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -385,22 +436,20 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
|
||||
if (typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
if (value.length === 0) return <span className="text-muted-foreground">None</span>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{value.map((item, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-100 rounded-sm text-xs">
|
||||
<Badge key={index} variant="secondary">
|
||||
{typeof item === "object" ? JSON.stringify(item) : String(item)}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className="bg-gray-100 p-2 rounded-sm text-xs overflow-auto mt-1">{JSON.stringify(value, null, 2)}</pre>
|
||||
);
|
||||
return <pre className="mt-1 overflow-auto rounded-sm bg-muted p-2 text-xs">{JSON.stringify(value, null, 2)}</pre>;
|
||||
}
|
||||
|
||||
return <span>{String(value)}</span>;
|
||||
|
|
@ -408,8 +457,8 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Spin size="large" />
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<UiLoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -417,7 +466,9 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
if (!settings) {
|
||||
return (
|
||||
<Card>
|
||||
<Text>No settings available or you do not have permission to view them.</Text>
|
||||
<CardContent>
|
||||
<p>No settings available or you do not have permission to view them.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -427,7 +478,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
const { values, field_schema } = settings;
|
||||
|
||||
if (!field_schema || !field_schema.properties) {
|
||||
return <Text>No schema information available</Text>;
|
||||
return <p>No schema information available</p>;
|
||||
}
|
||||
|
||||
return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => {
|
||||
|
|
@ -435,16 +486,14 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
|
||||
return (
|
||||
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
|
||||
<Text className="font-medium text-lg">{displayName}</Text>
|
||||
<Paragraph className="text-sm text-gray-500 mt-1">
|
||||
{property.description || "No description available"}
|
||||
</Paragraph>
|
||||
<div key={key} className="mb-6 border-b border-border pb-6 last:border-0">
|
||||
<p className="text-lg font-medium">{displayName}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{property.description || "No description available"}</p>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-2">{renderEditableField(key, property, value)}</div>
|
||||
) : (
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded-sm">{renderValue(key, value)}</div>
|
||||
<div className="mt-1 rounded-sm bg-muted p-2">{renderValue(key, value)}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -453,38 +502,38 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
|
|||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Default User Settings</Title>
|
||||
{!loading &&
|
||||
settings &&
|
||||
(isEditing ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditedValues(settings.values || {});
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSaveSettings} loading={saving}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="primary" onClick={() => setIsEditing(true)}>
|
||||
Edit Settings
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Default User Settings</h2>
|
||||
{!loading &&
|
||||
settings &&
|
||||
(isEditing ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditedValues(settings.values || {});
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveSettings} disabled={saving}>
|
||||
{saving && <UiLoadingSpinner className="size-4" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{settings?.field_schema?.description && (
|
||||
<Paragraph className="mb-4">{settings.field_schema.description}</Paragraph>
|
||||
)}
|
||||
<Divider />
|
||||
{settings?.field_schema?.description && <p className="mb-4">{settings.field_schema.description}</p>}
|
||||
<Separator />
|
||||
|
||||
<div className="mt-4 space-y-4">{renderSettings()}</div>
|
||||
<div className="mt-4 space-y-4">{renderSettings()}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Button } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import BulkEditUserModal from "./BulkEditUsers";
|
||||
import { CreateUserButton } from "@/components/CreateUserButton";
|
||||
import EditUserModal from "./edit_user";
|
||||
|
|
@ -34,7 +33,8 @@ import DefaultUserSettings from "./DefaultUserSettings";
|
|||
import { UsersTable } from "./view_users/UsersTable";
|
||||
import UserInfoView from "./view_users/user_info_view";
|
||||
import { UserInfo } from "@/components/networking";
|
||||
import { Skeleton } from "antd";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface ViewUserDashboardProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -353,9 +353,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
<div className="flex space-x-3">
|
||||
{userListQuery.isLoading && (
|
||||
<>
|
||||
<Skeleton.Button active size="default" shape="default" style={{ width: 110, height: 36 }} />
|
||||
<Skeleton.Button active size="default" shape="default" style={{ width: 145, height: 36 }} />
|
||||
<Skeleton.Button active size="default" shape="default" style={{ width: 110, height: 36 }} />
|
||||
<Skeleton className="h-9 w-[110px]" />
|
||||
<Skeleton className="h-9 w-[145px]" />
|
||||
<Skeleton className="h-9 w-[110px]" />
|
||||
</>
|
||||
)}
|
||||
{!userListQuery.isLoading && userID && accessToken && (
|
||||
|
|
@ -372,8 +372,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
{isProxyAdmin && (
|
||||
<Button
|
||||
onClick={handleToggleSelectionMode}
|
||||
type={selectionMode ? "primary" : "default"}
|
||||
className="flex items-center"
|
||||
variant={selectionMode ? "default" : "outline"}
|
||||
data-testid="toggle-user-selection"
|
||||
>
|
||||
{selectionMode ? "Cancel Selection" : "Select Users"}
|
||||
|
|
@ -382,10 +381,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
{isProxyAdmin && selectionMode && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setIsBulkEditModalVisible(true)}
|
||||
disabled={selectedUsers.length === 0}
|
||||
className="flex items-center"
|
||||
data-testid="bulk-edit-users"
|
||||
>
|
||||
Bulk Edit ({selectedUsers.length} selected)
|
||||
|
|
@ -397,31 +394,39 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
</div>
|
||||
|
||||
{isProxyAdmin ? (
|
||||
<TabGroup defaultIndex={0}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Users</Tab>
|
||||
<Tab>Default User Settings</Tab>
|
||||
</TabList>
|
||||
<Tabs defaultValue="users">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="users" className="flex-none">
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings" className="flex-none">
|
||||
Default User Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>{usersTable}</TabPanel>
|
||||
<TabsContent value="users">{usersTable}</TabsContent>
|
||||
|
||||
<TabPanel>
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
<TabsContent value="settings">
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="w-full max-w-md space-y-3">
|
||||
<Skeleton className="h-5 w-1/3" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
) : (
|
||||
<DefaultUserSettings
|
||||
accessToken={accessToken}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
) : (
|
||||
<DefaultUserSettings
|
||||
accessToken={accessToken}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
usersTable
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue