mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): migrate users views + user_info detail to shadcn
Migrates view_users (list dashboard), view_users/user_info_view (user detail + edit + add-to-team modal), and user_agent_activity (DAU/WAU/ MAU analytics) off Tremor TabGroup/Card/Grid/Text/Title/Metric and off antd Modal/Form/Select/Tooltip/Button/Skeleton. All three now use shadcn Tabs + Card + Dialog + react-hook-form (in the add-team modal), keeping the existing Tremor BarChart (chart exemption) and Tag-chip multi-select shim. Tests updated to use Radix tab roles and role='alertdialog' for the DeleteResourceModal selector. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
b861fd3fd4
commit
7546589e82
5 changed files with 967 additions and 682 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
|
||||
import UserAgentActivity from "./user_agent_activity";
|
||||
import * as networking from "./networking";
|
||||
|
|
@ -146,6 +147,7 @@ describe("UserAgentActivity", () => {
|
|||
});
|
||||
|
||||
it("should switch between DAU, WAU, and MAU tabs", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<UserAgentActivity {...defaultProps} />);
|
||||
|
||||
// Wait for data to load
|
||||
|
|
@ -158,20 +160,16 @@ describe("UserAgentActivity", () => {
|
|||
// Check default DAU tab content
|
||||
expect(screen.getByText("Daily Active Users - Last 7 Days")).toBeInTheDocument();
|
||||
|
||||
// Find all WAU tab buttons (there might be multiple)
|
||||
const wauTabs = screen.getAllByText("WAU");
|
||||
fireEvent.click(wauTabs[0]);
|
||||
// Switch to WAU tab (use role=tab to target the actual Radix tab trigger)
|
||||
await user.click(screen.getByRole("tab", { name: /^WAU$/ }));
|
||||
|
||||
// Check WAU tab content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Weekly Active Users - Last 7 Weeks")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find all MAU tab buttons
|
||||
const mauTabs = screen.getAllByText("MAU");
|
||||
fireEvent.click(mauTabs[0]);
|
||||
// Switch to MAU tab
|
||||
await user.click(screen.getByRole("tab", { name: /^MAU$/ }));
|
||||
|
||||
// Check MAU tab content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Monthly Active Users - Last 7 Months")).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,27 +1,37 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
BarChart,
|
||||
Metric,
|
||||
Subtitle,
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@tremor/react";
|
||||
import { Select, Tooltip } from "antd";
|
||||
import { userAgentSummaryCall, tagDauCall, tagWauCall, tagMauCall, tagDistinctCall } from "./networking";
|
||||
import PerUserUsage from "./per_user_usage";
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip as ShTooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { DateRangePickerValue } from "@tremor/react";
|
||||
import { BarChart, DateRangePickerValue } from "@tremor/react";
|
||||
|
||||
import {
|
||||
userAgentSummaryCall,
|
||||
tagDauCall,
|
||||
tagWauCall,
|
||||
tagMauCall,
|
||||
tagDistinctCall,
|
||||
} from "./networking";
|
||||
import PerUserUsage from "./per_user_usage";
|
||||
import { ChartLoader } from "./shared/chart_loader";
|
||||
|
||||
// New interfaces for the updated API response
|
||||
interface TagActiveUsersResponse {
|
||||
tag: string;
|
||||
active_users: number;
|
||||
|
|
@ -52,41 +62,120 @@ interface DistinctTagResponse {
|
|||
tag: string;
|
||||
}
|
||||
|
||||
interface DistinctTagsResponse {
|
||||
results: DistinctTagResponse[];
|
||||
}
|
||||
|
||||
interface UserAgentActivityProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
dateValue: DateRangePickerValue;
|
||||
onDateChange?: (value: DateRangePickerValue) => void; // Optional - not used anymore
|
||||
onDateChange?: (value: DateRangePickerValue) => void;
|
||||
}
|
||||
|
||||
const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, userRole, dateValue, onDateChange }) => {
|
||||
// Maximum number of categories to show in charts to prevent color palette overflow
|
||||
/**
|
||||
* shadcn Select + badge chip multi-select for tag filtering.
|
||||
*/
|
||||
function TagMultiSelect({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
}: {
|
||||
value: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
options: { label: string; value: string; title?: string }[];
|
||||
placeholder: string;
|
||||
}) {
|
||||
const selected = useMemo(() => value ?? [], [value]);
|
||||
const remaining = useMemo(
|
||||
() => options.filter((o) => !selected.includes(o.value)),
|
||||
[options, selected],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(v) => {
|
||||
if (v) onChange([...selected, v]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{remaining.length === 0 ? (
|
||||
<div className="py-2 px-3 text-sm text-muted-foreground">
|
||||
No more options
|
||||
</div>
|
||||
) : (
|
||||
remaining.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} title={opt.title}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selected.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selected.map((v) => {
|
||||
const opt = options.find((o) => o.value === v);
|
||||
return (
|
||||
<Badge
|
||||
key={v}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 max-w-[240px]"
|
||||
>
|
||||
<span className="truncate">{opt?.label ?? v}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(selected.filter((s) => s !== v))}
|
||||
className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
|
||||
aria-label={`Remove ${opt?.label ?? v}`}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
dateValue,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onDateChange,
|
||||
}) => {
|
||||
const MAX_CATEGORIES = 10;
|
||||
|
||||
// Separate state for each endpoint
|
||||
const [dauData, setDauData] = useState<ActiveUsersAnalyticsResponse>({ results: [] });
|
||||
const [wauData, setWauData] = useState<ActiveUsersAnalyticsResponse>({ results: [] });
|
||||
const [mauData, setMauData] = useState<ActiveUsersAnalyticsResponse>({ results: [] });
|
||||
const [summaryData, setSummaryData] = useState<TagSummaryResponse>({ results: [] });
|
||||
const [dauData, setDauData] = useState<ActiveUsersAnalyticsResponse>({
|
||||
results: [],
|
||||
});
|
||||
const [wauData, setWauData] = useState<ActiveUsersAnalyticsResponse>({
|
||||
results: [],
|
||||
});
|
||||
const [mauData, setMauData] = useState<ActiveUsersAnalyticsResponse>({
|
||||
results: [],
|
||||
});
|
||||
const [summaryData, setSummaryData] = useState<TagSummaryResponse>({
|
||||
results: [],
|
||||
});
|
||||
|
||||
const [userAgentFilter, setUserAgentFilter] = useState<string>("");
|
||||
const [userAgentFilter] = useState<string>("");
|
||||
|
||||
// Tag filtering state
|
||||
const [availableTags, setAvailableTags] = useState<string[]>([]);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [tagsLoading, setTagsLoading] = useState(false);
|
||||
|
||||
// Separate loading states for each endpoint
|
||||
const [dauLoading, setDauLoading] = useState(false);
|
||||
const [wauLoading, setWauLoading] = useState(false);
|
||||
const [mauLoading, setMauLoading] = useState(false);
|
||||
const [summaryLoading, setSummaryLoading] = useState(false);
|
||||
|
||||
// Use today's date as the end date for all API calls
|
||||
const today = new Date();
|
||||
|
||||
const fetchAvailableTags = async () => {
|
||||
|
|
@ -179,12 +268,11 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
}
|
||||
};
|
||||
|
||||
// Effect to fetch available tags on mount
|
||||
useEffect(() => {
|
||||
fetchAvailableTags();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accessToken]);
|
||||
|
||||
// Effect for DAU/WAU/MAU data (independent of date picker)
|
||||
useEffect(() => {
|
||||
if (!accessToken) return;
|
||||
|
||||
|
|
@ -195,9 +283,9 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
}, 50);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accessToken, userAgentFilter, selectedTags]);
|
||||
|
||||
// Effect for summary data (depends on date picker)
|
||||
useEffect(() => {
|
||||
if (!dateValue.from || !dateValue.to) return;
|
||||
|
||||
|
|
@ -206,9 +294,9 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
}, 50);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accessToken, dateValue, selectedTags]);
|
||||
|
||||
// Helper function to extract user agent from tag
|
||||
const extractUserAgent = (tag: string): string => {
|
||||
if (tag.startsWith("User-Agent: ")) {
|
||||
return tag.replace("User-Agent: ", "");
|
||||
|
|
@ -216,7 +304,6 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
return tag;
|
||||
};
|
||||
|
||||
// Helper function to truncate user agent name (used with Ant Design Tooltip)
|
||||
const truncateUserAgent = (userAgent: string): string => {
|
||||
if (userAgent.length > 15) {
|
||||
return userAgent.substring(0, 15) + "...";
|
||||
|
|
@ -224,9 +311,7 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
return userAgent;
|
||||
};
|
||||
|
||||
// Get all user agents for each chart type based on their specific data
|
||||
const getAllTagsForData = (data: TagActiveUsersResponse[]) => {
|
||||
// Aggregate total active users per tag
|
||||
const tagTotals = data.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.tag] = (acc[item.tag] || 0) + item.active_users;
|
||||
|
|
@ -235,7 +320,6 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
// Sort by total active users and return all tags
|
||||
return Object.entries(tagTotals)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([tag]) => tag);
|
||||
|
|
@ -245,20 +329,17 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
const allWauTags = getAllTagsForData(wauData.results).slice(0, MAX_CATEGORIES);
|
||||
const allMauTags = getAllTagsForData(mauData.results).slice(0, MAX_CATEGORIES);
|
||||
|
||||
// Prepare daily chart data (DAU) - always show last 7 days
|
||||
const generateDailyChartData = () => {
|
||||
const chartData: any[] = [];
|
||||
const endDate = new Date();
|
||||
|
||||
// Generate all 7 days
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date(endDate);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split("T")[0]; // YYYY-MM-DD format
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
|
||||
const dayEntry: any = { date: dateStr };
|
||||
|
||||
// Initialize all user agents to 0
|
||||
allDauTags.forEach((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
dayEntry[userAgent] = 0;
|
||||
|
|
@ -267,7 +348,6 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
chartData.push(dayEntry);
|
||||
}
|
||||
|
||||
// Fill in actual data
|
||||
dauData.results.forEach((item) => {
|
||||
const userAgent = extractUserAgent(item.tag);
|
||||
const dayEntry = chartData.find((d) => d.date === item.date);
|
||||
|
|
@ -281,15 +361,12 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
|
||||
const dailyChartData = generateDailyChartData();
|
||||
|
||||
// Prepare weekly chart data (WAU) - always show all 7 weeks
|
||||
const generateWeeklyChartData = () => {
|
||||
const chartData: any[] = [];
|
||||
|
||||
// Generate all 7 weeks (Week 1 through Week 7)
|
||||
for (let weekNum = 1; weekNum <= 7; weekNum++) {
|
||||
const weekEntry: any = { week: `Week ${weekNum}` };
|
||||
|
||||
// Initialize all user agents to 0
|
||||
allWauTags.forEach((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
weekEntry[userAgent] = 0;
|
||||
|
|
@ -298,10 +375,8 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
chartData.push(weekEntry);
|
||||
}
|
||||
|
||||
// Fill in actual data
|
||||
wauData.results.forEach((item) => {
|
||||
const userAgent = extractUserAgent(item.tag);
|
||||
// Extract week number from the date field (e.g., "Week 1 (Jul 27)" -> "Week 1")
|
||||
const weekMatch = item.date.match(/Week (\d+)/);
|
||||
if (weekMatch) {
|
||||
const weekLabel = `Week ${weekMatch[1]}`;
|
||||
|
|
@ -317,15 +392,12 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
|
||||
const weeklyChartData = generateWeeklyChartData();
|
||||
|
||||
// Prepare monthly chart data (MAU) - always show all 7 months
|
||||
const generateMonthlyChartData = () => {
|
||||
const chartData: any[] = [];
|
||||
|
||||
// Generate all 7 months (Month 1 through Month 7)
|
||||
for (let monthNum = 1; monthNum <= 7; monthNum++) {
|
||||
const monthEntry: any = { month: `Month ${monthNum}` };
|
||||
|
||||
// Initialize all user agents to 0
|
||||
allMauTags.forEach((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
monthEntry[userAgent] = 0;
|
||||
|
|
@ -334,10 +406,8 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
chartData.push(monthEntry);
|
||||
}
|
||||
|
||||
// Fill in actual data
|
||||
mauData.results.forEach((item) => {
|
||||
const userAgent = extractUserAgent(item.tag);
|
||||
// Extract month number from the date field (e.g., "Month 1 (Jul)" -> "Month 1")
|
||||
const monthMatch = item.date.match(/Month (\d+)/);
|
||||
if (monthMatch) {
|
||||
const monthLabel = `Month ${monthMatch[1]}`;
|
||||
|
|
@ -353,8 +423,10 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
|
||||
const monthlyChartData = generateMonthlyChartData();
|
||||
|
||||
// Format numbers with K, M abbreviations
|
||||
const formatAbbreviatedNumber = (value: number, decimalPlaces: number = 0): string => {
|
||||
const formatAbbreviatedNumber = (
|
||||
value: number,
|
||||
decimalPlaces: number = 0,
|
||||
): string => {
|
||||
if (value >= 100000000) {
|
||||
return (value / 1000000).toFixed(decimalPlaces) + "M";
|
||||
} else if (value >= 10000000) {
|
||||
|
|
@ -370,197 +442,228 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
}
|
||||
};
|
||||
|
||||
const tagOptions = availableTags.map((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
const displayName =
|
||||
userAgent.length > 50 ? `${userAgent.substring(0, 50)}...` : userAgent;
|
||||
return { label: displayName, value: tag, title: userAgent };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 mt-6">
|
||||
{/* Summary Section Card */}
|
||||
<Card>
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<Title>Summary by User Agent</Title>
|
||||
<Subtitle>Performance metrics for different user agents</Subtitle>
|
||||
<h2 className="text-2xl font-semibold m-0">
|
||||
Summary by User Agent
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground m-0">
|
||||
Performance metrics for different user agents
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* User Agent Filter */}
|
||||
<div className="w-96">
|
||||
<Text className="text-sm font-medium block mb-2">Filter by User Agents</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="All User Agents"
|
||||
<Label className="text-sm font-medium block mb-2">
|
||||
Filter by User Agents
|
||||
</Label>
|
||||
<TagMultiSelect
|
||||
value={selectedTags}
|
||||
onChange={setSelectedTags}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
allowClear={true}
|
||||
loading={tagsLoading}
|
||||
optionFilterProp="label"
|
||||
className="rounded-md"
|
||||
maxTagCount="responsive"
|
||||
>
|
||||
{availableTags.map((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
const displayName = userAgent.length > 50 ? `${userAgent.substring(0, 50)}...` : userAgent;
|
||||
return (
|
||||
<Select.Option key={tag} value={tag} label={displayName} title={userAgent}>
|
||||
{displayName}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
options={tagOptions}
|
||||
placeholder="All User Agents"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Range Picker is controlled by parent component */}
|
||||
|
||||
{/* Top 4 User Agents Cards */}
|
||||
{summaryLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<Grid numItems={4} className="gap-4">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{(summaryData.results || []).slice(0, 4).map((tag, index) => {
|
||||
const userAgent = extractUserAgent(tag.tag);
|
||||
const displayName = truncateUserAgent(userAgent);
|
||||
return (
|
||||
<Card key={index}>
|
||||
<Tooltip title={userAgent} placement="top">
|
||||
<Title className="truncate">{displayName}</Title>
|
||||
</Tooltip>
|
||||
<Card key={index} className="p-4">
|
||||
<TooltipProvider>
|
||||
<ShTooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<h3 className="text-lg font-semibold truncate">
|
||||
{displayName}
|
||||
</h3>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{userAgent}</TooltipContent>
|
||||
</ShTooltip>
|
||||
</TooltipProvider>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Success Requests</Text>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(tag.successful_requests)}</Metric>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Success Requests
|
||||
</span>
|
||||
<div className="text-lg font-semibold">
|
||||
{formatAbbreviatedNumber(tag.successful_requests)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Tokens</Text>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(tag.total_tokens)}</Metric>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Tokens
|
||||
</span>
|
||||
<div className="text-lg font-semibold">
|
||||
{formatAbbreviatedNumber(tag.total_tokens)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Cost</Text>
|
||||
<Metric className="text-lg">${formatAbbreviatedNumber(tag.total_spend, 4)}</Metric>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Cost
|
||||
</span>
|
||||
<div className="text-lg font-semibold">
|
||||
${formatAbbreviatedNumber(tag.total_spend, 4)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{/* Fill remaining slots if less than 4 agents */}
|
||||
{Array.from({ length: Math.max(0, 4 - (summaryData.results || []).length) }).map((_, index) => (
|
||||
<Card key={`empty-${index}`}>
|
||||
<Title>No Data</Title>
|
||||
{Array.from({
|
||||
length: Math.max(0, 4 - (summaryData.results || []).length),
|
||||
}).map((_, index) => (
|
||||
<Card key={`empty-${index}`} className="p-4">
|
||||
<h3 className="text-lg font-semibold">No Data</h3>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Success Requests</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Success Requests
|
||||
</span>
|
||||
<div className="text-lg font-semibold">-</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Tokens</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Tokens
|
||||
</span>
|
||||
<div className="text-lg font-semibold">-</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Cost</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Cost
|
||||
</span>
|
||||
<div className="text-lg font-semibold">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */}
|
||||
<Card>
|
||||
<TabGroup>
|
||||
<TabList className="mb-6">
|
||||
<Tab>DAU/WAU/MAU</Tab>
|
||||
<Tab>Per User Usage (Last 30 Days)</Tab>
|
||||
</TabList>
|
||||
{/* Main Tabs for DAU/WAU/MAU vs Per User Usage */}
|
||||
<Card className="p-6">
|
||||
<Tabs defaultValue="dau-wau-mau">
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="dau-wau-mau">DAU/WAU/MAU</TabsTrigger>
|
||||
<TabsTrigger value="per-user">
|
||||
Per User Usage (Last 30 Days)
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
{/* DAU/WAU/MAU Tab Panel */}
|
||||
<TabPanel>
|
||||
<div className="mb-6">
|
||||
<Title>DAU, WAU & MAU per Agent</Title>
|
||||
<Subtitle>Active users across different time periods</Subtitle>
|
||||
</div>
|
||||
<TabsContent value="dau-wau-mau">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-semibold m-0">
|
||||
DAU, WAU & MAU per Agent
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground m-0">
|
||||
Active users across different time periods
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-6">
|
||||
<Tab>DAU</Tab>
|
||||
<Tab>WAU</Tab>
|
||||
<Tab>MAU</Tab>
|
||||
</TabList>
|
||||
<Tabs defaultValue="dau">
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="dau">DAU</TabsTrigger>
|
||||
<TabsTrigger value="wau">WAU</TabsTrigger>
|
||||
<TabsTrigger value="mau">MAU</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="mb-4">
|
||||
<Title className="text-lg">Daily Active Users - Last 7 Days</Title>
|
||||
</div>
|
||||
{dauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={dailyChartData}
|
||||
index="date"
|
||||
categories={allDauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabsContent value="dau">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Daily Active Users - Last 7 Days
|
||||
</h3>
|
||||
</div>
|
||||
{dauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={dailyChartData}
|
||||
index="date"
|
||||
categories={allDauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) =>
|
||||
formatAbbreviatedNumber(value)
|
||||
}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabPanel>
|
||||
<div className="mb-4">
|
||||
<Title className="text-lg">Weekly Active Users - Last 7 Weeks</Title>
|
||||
</div>
|
||||
{wauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={weeklyChartData}
|
||||
index="week"
|
||||
categories={allWauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabsContent value="wau">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Weekly Active Users - Last 7 Weeks
|
||||
</h3>
|
||||
</div>
|
||||
{wauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={weeklyChartData}
|
||||
index="week"
|
||||
categories={allWauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) =>
|
||||
formatAbbreviatedNumber(value)
|
||||
}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabPanel>
|
||||
<div className="mb-4">
|
||||
<Title className="text-lg">Monthly Active Users - Last 7 Months</Title>
|
||||
</div>
|
||||
{mauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={monthlyChartData}
|
||||
index="month"
|
||||
categories={allMauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabPanel>
|
||||
<TabsContent value="mau">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Monthly Active Users - Last 7 Months
|
||||
</h3>
|
||||
</div>
|
||||
{mauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={monthlyChartData}
|
||||
index="month"
|
||||
categories={allMauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) =>
|
||||
formatAbbreviatedNumber(value)
|
||||
}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
|
||||
{/* Per User Usage Tab Panel */}
|
||||
<TabPanel>
|
||||
<PerUserUsage
|
||||
accessToken={accessToken}
|
||||
selectedTags={selectedTags}
|
||||
formatAbbreviatedNumber={formatAbbreviatedNumber}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
<TabsContent value="per-user">
|
||||
<PerUserUsage
|
||||
accessToken={accessToken}
|
||||
selectedTags={selectedTags}
|
||||
formatAbbreviatedNumber={formatAbbreviatedNumber}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
import BulkEditUserModal from "./BulkEditUsers";
|
||||
import { CreateUserButton } from "./CreateUserButton";
|
||||
import DefaultUserSettings from "./DefaultUserSettings";
|
||||
import EditUserModal from "./edit_user";
|
||||
import {
|
||||
getPossibleUserRoles,
|
||||
|
|
@ -20,17 +22,12 @@ import { updateExistingKeys } from "@/utils/dataUtils";
|
|||
import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
|
||||
import { useDebouncedState } from "@tanstack/react-pacer/debouncer";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Typography } from "antd";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { modelAvailableCall, userDeleteCall } from "./networking";
|
||||
import DefaultUserSettings from "./DefaultUserSettings";
|
||||
import { columns } from "./view_users/columns";
|
||||
import { UserDataTable } from "./view_users/table";
|
||||
import { UserInfo } from "./view_users/types";
|
||||
import { Skeleton } from "antd";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
interface ViewUserDashboardProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -40,7 +37,9 @@ interface ViewUserDashboardProps {
|
|||
userID: string | null;
|
||||
teams: any[] | null;
|
||||
setKeys: React.Dispatch<React.SetStateAction<object[] | null>>;
|
||||
orgAdminOrgIds?: Array<{organization_id: string, organization_alias: string}> | null;
|
||||
orgAdminOrgIds?:
|
||||
| Array<{ organization_id: string; organization_alias: string }>
|
||||
| null;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
|
|
@ -71,7 +70,14 @@ const initialFilters: FilterState = {
|
|||
sort_order: "desc",
|
||||
};
|
||||
|
||||
const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, token, userRole, userID, teams, orgAdminOrgIds }) => {
|
||||
const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||
accessToken,
|
||||
token,
|
||||
userRole,
|
||||
userID,
|
||||
teams,
|
||||
orgAdminOrgIds,
|
||||
}) => {
|
||||
const isProxyAdmin = userRole ? isProxyAdminRole(userRole) : false;
|
||||
const queryClient = useQueryClient();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
|
@ -80,11 +86,15 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isDeletingUser, setIsDeletingUser] = useState(false);
|
||||
const [userToDelete, setUserToDelete] = useState<UserInfo | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("users");
|
||||
const [filters, setFilters] = useState<FilterState>(initialFilters);
|
||||
const [debouncedFilters, setDebouncedFilters, debouncer] = useDebouncedState(filters, { wait: 300 });
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
|
||||
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null);
|
||||
const [debouncedFilters, setDebouncedFilters, debouncer] = useDebouncedState(
|
||||
filters,
|
||||
{ wait: 300 },
|
||||
);
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
|
||||
useState(false);
|
||||
const [invitationLinkData, setInvitationLinkData] =
|
||||
useState<InvitationLink | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState<string | null>(null);
|
||||
const [selectedUsers, setSelectedUsers] = useState<UserInfo[]>([]);
|
||||
const [isBulkEditModalVisible, setIsBulkEditModalVisible] = useState(false);
|
||||
|
|
@ -106,7 +116,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
setBaseUrl(getProxyBaseUrl());
|
||||
}, []);
|
||||
|
||||
// Fetch available models for bulk edit
|
||||
useEffect(() => {
|
||||
const fetchUserModels = async () => {
|
||||
try {
|
||||
|
|
@ -114,8 +123,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
return;
|
||||
}
|
||||
|
||||
const model_available = await modelAvailableCall(accessToken, userID, userRole);
|
||||
let available_model_names = model_available["data"].map((element: { id: string }) => element.id);
|
||||
const model_available = await modelAvailableCall(
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
);
|
||||
let available_model_names = model_available["data"].map(
|
||||
(element: { id: string }) => element.id,
|
||||
);
|
||||
console.log("available_model_names:", available_model_names);
|
||||
setUserModels(available_model_names);
|
||||
} catch (error) {
|
||||
|
|
@ -159,12 +174,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
setIsDeletingUser(true);
|
||||
await userDeleteCall(accessToken, [userToDelete.user_id]);
|
||||
|
||||
// Update the user list after deletion
|
||||
queryClient.setQueriesData<UserListResponse>({ queryKey: ["userList"] }, (previousData) => {
|
||||
if (previousData === undefined) return previousData;
|
||||
const updatedUsers = previousData.users.filter((user) => user.user_id !== userToDelete.user_id);
|
||||
return { ...previousData, users: updatedUsers };
|
||||
});
|
||||
queryClient.setQueriesData<UserListResponse>(
|
||||
{ queryKey: ["userList"] },
|
||||
(previousData) => {
|
||||
if (previousData === undefined) return previousData;
|
||||
const updatedUsers = previousData.users.filter(
|
||||
(user) => user.user_id !== userToDelete.user_id,
|
||||
);
|
||||
return { ...previousData, users: updatedUsers };
|
||||
},
|
||||
);
|
||||
|
||||
NotificationsManager.success("User deleted successfully");
|
||||
} catch (error) {
|
||||
|
|
@ -197,25 +216,29 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
|
||||
try {
|
||||
const response = await userUpdateUserCall(accessToken, editedUser, null);
|
||||
queryClient.setQueriesData<UserListResponse>({ queryKey: ["userList"] }, (previousData) => {
|
||||
if (previousData === undefined) return previousData;
|
||||
const updatedUsers = previousData.users.map((user) => {
|
||||
if (user.user_id === response.data.user_id) {
|
||||
return updateExistingKeys(user, response.data);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
queryClient.setQueriesData<UserListResponse>(
|
||||
{ queryKey: ["userList"] },
|
||||
(previousData) => {
|
||||
if (previousData === undefined) return previousData;
|
||||
const updatedUsers = previousData.users.map((user) => {
|
||||
if (user.user_id === response.data.user_id) {
|
||||
return updateExistingKeys(user, response.data);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
|
||||
return { ...previousData, users: updatedUsers };
|
||||
});
|
||||
return { ...previousData, users: updatedUsers };
|
||||
},
|
||||
);
|
||||
|
||||
NotificationsManager.success(`User ${editedUser.user_id} updated successfully`);
|
||||
NotificationsManager.success(
|
||||
`User ${editedUser.user_id} updated successfully`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("There was an error updating the user", error);
|
||||
}
|
||||
setSelectedUser(null);
|
||||
setEditModalVisible(false);
|
||||
// Close the modal
|
||||
};
|
||||
|
||||
const handlePageChange = async (newPage: number) => {
|
||||
|
|
@ -241,14 +264,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
};
|
||||
|
||||
const handleBulkEditSuccess = () => {
|
||||
// Refresh the user list
|
||||
queryClient.invalidateQueries({ queryKey: ["userList"] });
|
||||
setSelectedUsers([]);
|
||||
setSelectionMode(false);
|
||||
};
|
||||
|
||||
const userListQuery = useQuery({
|
||||
queryKey: ["userList", { debouncedFilter: debouncedFilters, currentPage, orgAdminOrgIds }],
|
||||
queryKey: [
|
||||
"userList",
|
||||
{ debouncedFilter: debouncedFilters, currentPage, orgAdminOrgIds },
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!accessToken) throw new Error("Access token required");
|
||||
|
||||
|
|
@ -263,7 +288,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
debouncedFilters.sso_user_id || null,
|
||||
debouncedFilters.sort_by,
|
||||
debouncedFilters.sort_order,
|
||||
orgAdminOrgIds ? orgAdminOrgIds.map((o) => o.organization_id) : null,
|
||||
orgAdminOrgIds
|
||||
? orgAdminOrgIds.map((o) => o.organization_id)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
enabled: Boolean(accessToken && token && userRole && userID),
|
||||
|
|
@ -290,27 +317,60 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
},
|
||||
handleDelete,
|
||||
handleResetPassword,
|
||||
() => { }, // placeholder function, will be overridden in UserDataTable
|
||||
() => {}, // placeholder function, will be overridden in UserDataTable
|
||||
);
|
||||
|
||||
const dataTableProps = {
|
||||
data: userListQuery.data?.users || [],
|
||||
columns: tableColumns,
|
||||
isLoading: userListQuery.isLoading,
|
||||
accessToken,
|
||||
userRole,
|
||||
onSortChange: handleSortChange,
|
||||
currentSort: {
|
||||
sortBy: filters.sort_by,
|
||||
sortOrder: filters.sort_order,
|
||||
},
|
||||
possibleUIRoles,
|
||||
handleEdit: (user: UserInfo) => {
|
||||
setSelectedUser(user);
|
||||
setEditModalVisible(true);
|
||||
},
|
||||
handleDelete,
|
||||
handleResetPassword,
|
||||
onSelectionChange: handleSelectionChange,
|
||||
filters,
|
||||
updateFilters,
|
||||
initialFilters,
|
||||
teams,
|
||||
userListResponse,
|
||||
currentPage,
|
||||
handlePageChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full p-8 overflow-hidden">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<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]" />
|
||||
</>
|
||||
) : userID && accessToken ? (
|
||||
<>
|
||||
<CreateUserButton userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
|
||||
<CreateUserButton
|
||||
userID={userID}
|
||||
accessToken={accessToken}
|
||||
teams={teams}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
/>
|
||||
|
||||
{isProxyAdmin && (
|
||||
<Button
|
||||
onClick={handleToggleSelectionMode}
|
||||
type={selectionMode ? "primary" : "default"}
|
||||
variant={selectionMode ? "default" : "outline"}
|
||||
className="flex items-center"
|
||||
>
|
||||
{selectionMode ? "Cancel Selection" : "Select Users"}
|
||||
|
|
@ -318,7 +378,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
)}
|
||||
|
||||
{isProxyAdmin && selectionMode && (
|
||||
<Button type="primary" onClick={handleBulkEdit} disabled={selectedUsers.length === 0} className="flex items-center">
|
||||
<Button
|
||||
onClick={handleBulkEdit}
|
||||
disabled={selectedUsers.length === 0}
|
||||
className="flex items-center"
|
||||
>
|
||||
Bulk Edit ({selectedUsers.length} selected)
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -328,90 +392,43 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
</div>
|
||||
|
||||
{isProxyAdmin ? (
|
||||
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "users" : "settings")}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Users</Tab>
|
||||
<Tab>Default User Settings</Tab>
|
||||
</TabList>
|
||||
<Tabs defaultValue="users">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="users">Users</TabsTrigger>
|
||||
<TabsTrigger value="settings">Default User Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<UserDataTable
|
||||
data={userListQuery.data?.users || []}
|
||||
columns={tableColumns}
|
||||
isLoading={userListQuery.isLoading}
|
||||
<TabsContent value="users">
|
||||
<UserDataTable
|
||||
{...dataTableProps}
|
||||
enableSelection={selectionMode}
|
||||
selectedUsers={selectedUsers}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings">
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div className="flex flex-col gap-3 p-6">
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
) : (
|
||||
<DefaultUserSettings
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
onSortChange={handleSortChange}
|
||||
currentSort={{
|
||||
sortBy: filters.sort_by,
|
||||
sortOrder: filters.sort_order,
|
||||
}}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
handleEdit={(user) => {
|
||||
setSelectedUser(user);
|
||||
setEditModalVisible(true);
|
||||
}}
|
||||
handleDelete={handleDelete}
|
||||
handleResetPassword={handleResetPassword}
|
||||
enableSelection={selectionMode}
|
||||
selectedUsers={selectedUsers}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
filters={filters}
|
||||
updateFilters={updateFilters}
|
||||
initialFilters={initialFilters}
|
||||
teams={teams}
|
||||
userListResponse={userListResponse}
|
||||
currentPage={currentPage}
|
||||
handlePageChange={handlePageChange}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
</div>
|
||||
) : (
|
||||
<DefaultUserSettings
|
||||
accessToken={accessToken}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<UserDataTable
|
||||
data={userListQuery.data?.users || []}
|
||||
columns={tableColumns}
|
||||
isLoading={userListQuery.isLoading}
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
onSortChange={handleSortChange}
|
||||
currentSort={{
|
||||
sortBy: filters.sort_by,
|
||||
sortOrder: filters.sort_order,
|
||||
}}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
handleEdit={(user) => {
|
||||
setSelectedUser(user);
|
||||
setEditModalVisible(true);
|
||||
}}
|
||||
handleDelete={handleDelete}
|
||||
handleResetPassword={handleResetPassword}
|
||||
{...dataTableProps}
|
||||
enableSelection={false}
|
||||
selectedUsers={[]}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
filters={filters}
|
||||
updateFilters={updateFilters}
|
||||
initialFilters={initialFilters}
|
||||
teams={teams}
|
||||
userListResponse={userListResponse}
|
||||
currentPage={currentPage}
|
||||
handlePageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -435,7 +452,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
|||
{
|
||||
label: "Global Proxy Role",
|
||||
value:
|
||||
(userToDelete && possibleUIRoles?.[userToDelete.user_role]?.ui_label) || userToDelete?.user_role || "-",
|
||||
(userToDelete &&
|
||||
possibleUIRoles?.[userToDelete.user_role]?.ui_label) ||
|
||||
userToDelete?.user_role ||
|
||||
"-",
|
||||
},
|
||||
{ label: "Total Spend (USD)", value: userToDelete?.spend?.toFixed(2) },
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,15 @@ describe("UserInfoView", () => {
|
|||
});
|
||||
|
||||
it("should render the user alias after loading", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<UserInfoView {...defaultProps} />);
|
||||
|
||||
// The user alias is shown on the Details tab; activate it first.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tab", { name: /details/i })).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByRole("tab", { name: /details/i }));
|
||||
|
||||
const aliases = await screen.findAllByText("Test Alias");
|
||||
expect(aliases.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
@ -203,9 +210,13 @@ describe("UserInfoView", () => {
|
|||
expect(screen.getByText("Remove from Team")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The DeleteResourceModal's OK button has text "Delete" - find it within the modal
|
||||
const modal = screen.getByText("Remove from Team").closest(".ant-modal") as HTMLElement;
|
||||
const deleteConfirmButton = within(modal).getByRole("button", { name: /delete/i });
|
||||
// The DeleteResourceModal's OK button has text "Delete" - find it within the alert dialog
|
||||
const modal = screen
|
||||
.getByText("Remove from Team")
|
||||
.closest('[role="alertdialog"]') as HTMLElement;
|
||||
const deleteConfirmButton = within(modal).getByRole("button", {
|
||||
name: /delete/i,
|
||||
});
|
||||
await user.click(deleteConfirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue