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:
Yuneng Jiang 2026-07-24 22:49:07 -07:00
parent ec0fe36974
commit fdafcf28b2
No known key found for this signature in database
5 changed files with 298 additions and 255 deletions

View file

@ -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
},

View file

@ -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>

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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
)}