mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): EntityUsageExport to shadcn
- ExportFormatSelector: antd Select \u2192 shadcn Select with explicit Label. - ExportTypeSelector: antd Radio.Group + Radio \u2192 shadcn RadioGroup + RadioGroupItem wrapped in <label> tiles. - ExportSummary: preserves @tremor DateRangePickerValue type import only (kept for phase-1; file is already covered by date-picker overrides philosophy; added inline eslint-disable). text-gray-500 \u2192 text-muted-foreground. - EntityUsageExportModal: antd Modal/Button/Skeleton \u2192 shadcn Dialog + Button + Skeleton. 'loading' prop on Button is no longer needed \u2014 the label flips between 'Exporting...' and the format name. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
6d182db494
commit
ba043e06a5
4 changed files with 176 additions and 95 deletions
|
|
@ -1,12 +1,23 @@
|
|||
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import { createTeamAliasMap } from "@/utils/teamUtils";
|
||||
import { Button, Modal, Skeleton } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import ExportFormatSelector from "./ExportFormatSelector";
|
||||
import ExportSummary from "./ExportSummary";
|
||||
import ExportTypeSelector from "./ExportTypeSelector";
|
||||
import type { EntityUsageExportModalProps, ExportFormat, ExportScope } from "./types";
|
||||
import type {
|
||||
EntityUsageExportModalProps,
|
||||
ExportFormat,
|
||||
ExportScope,
|
||||
} from "./types";
|
||||
import { handleExportCSV, handleExportJSON } from "./utils";
|
||||
|
||||
const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
|
||||
|
|
@ -26,18 +37,36 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
|
|||
const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
|
||||
const modalTitle = customTitle || `Export ${entityLabel} Usage`;
|
||||
|
||||
// Cache team alias map using useMemo
|
||||
const teamAliasMap = useMemo(() => createTeamAliasMap(teams), [teams]);
|
||||
|
||||
const handleExport = async (format?: ExportFormat) => {
|
||||
const formatToUse = format || exportFormat;
|
||||
setIsExporting(true);
|
||||
try {
|
||||
if (formatToUse === "csv") {
|
||||
handleExportCSV(spendData, exportScope, entityLabel, entityType, teamAliasMap);
|
||||
NotificationsManager.success(`${entityLabel} usage data exported successfully as CSV`);
|
||||
handleExportCSV(
|
||||
spendData,
|
||||
exportScope,
|
||||
entityLabel,
|
||||
entityType,
|
||||
teamAliasMap,
|
||||
);
|
||||
NotificationsManager.success(
|
||||
`${entityLabel} usage data exported successfully as CSV`,
|
||||
);
|
||||
} else {
|
||||
handleExportJSON(spendData, exportScope, entityLabel, entityType, dateRange, selectedFilters, teamAliasMap);
|
||||
NotificationsManager.success(`${entityLabel} usage data exported successfully as JSON`);
|
||||
handleExportJSON(
|
||||
spendData,
|
||||
exportScope,
|
||||
entityLabel,
|
||||
entityType,
|
||||
dateRange,
|
||||
selectedFilters,
|
||||
teamAliasMap,
|
||||
);
|
||||
NotificationsManager.success(
|
||||
`${entityLabel} usage data exported successfully as JSON`,
|
||||
);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
|
|
@ -49,45 +78,67 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<span className="text-base font-semibold">{modalTitle}</span>}
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={480}
|
||||
onOpenChange={(open) => (!open ? onClose() : undefined)}
|
||||
>
|
||||
<div className="space-y-5 py-2">
|
||||
{isLoadingTeams ? (
|
||||
<Skeleton active />
|
||||
) : (
|
||||
<>
|
||||
<ExportSummary dateRange={dateRange} selectedFilters={selectedFilters} />
|
||||
<ExportTypeSelector value={exportScope} onChange={setExportScope} entityType={entityType} />
|
||||
<ExportFormatSelector value={exportFormat} onChange={setExportFormat} />
|
||||
</>
|
||||
)}
|
||||
{isLoadingTeams ? (
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Skeleton.Button active />
|
||||
<Skeleton.Button active />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outlined" onClick={onClose} disabled={isExporting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleExport()}
|
||||
loading={isExporting || isLoadingTeams}
|
||||
disabled={isExporting || isLoadingTeams}
|
||||
type="primary"
|
||||
>
|
||||
{isExporting ? "Exporting..." : `Export ${exportFormat.toUpperCase()}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
<DialogContent className="max-w-[480px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-semibold">
|
||||
{modalTitle}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-5 py-2">
|
||||
{isLoadingTeams ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ExportSummary
|
||||
dateRange={dateRange}
|
||||
selectedFilters={selectedFilters}
|
||||
/>
|
||||
<ExportTypeSelector
|
||||
value={exportScope}
|
||||
onChange={setExportScope}
|
||||
entityType={entityType}
|
||||
/>
|
||||
<ExportFormatSelector
|
||||
value={exportFormat}
|
||||
onChange={setExportFormat}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isLoadingTeams ? (
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<Skeleton className="h-8 w-24" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isExporting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleExport()}
|
||||
disabled={isExporting || isLoadingTeams}
|
||||
>
|
||||
{isExporting
|
||||
? "Exporting..."
|
||||
: `Export ${exportFormat.toUpperCase()}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import React from "react";
|
||||
import { Select } from "antd";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { ExportFormat } from "./types";
|
||||
|
||||
interface ExportFormatSelectorProps {
|
||||
|
|
@ -7,28 +14,24 @@ interface ExportFormatSelectorProps {
|
|||
onChange: (value: ExportFormat) => void;
|
||||
}
|
||||
|
||||
const ExportFormatSelector: React.FC<ExportFormatSelectorProps> = ({ value, onChange }) => {
|
||||
const ExportFormatSelector: React.FC<ExportFormatSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 block mb-2">Format</label>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="w-full"
|
||||
options={[
|
||||
{
|
||||
value: "csv",
|
||||
label: "CSV (Excel, Google Sheets)",
|
||||
},
|
||||
{
|
||||
value: "json",
|
||||
label: "JSON (includes metadata)",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Label className="text-sm font-medium block mb-2">Format</Label>
|
||||
<Select value={value} onValueChange={(v) => onChange(v as ExportFormat)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="csv">CSV (Excel, Google Sheets)</SelectItem>
|
||||
<SelectItem value="json">JSON (includes metadata)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportFormatSelector;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import type { DateRangePickerValue } from "@tremor/react";
|
||||
|
||||
interface ExportSummaryProps {
|
||||
|
|
@ -6,14 +7,18 @@ interface ExportSummaryProps {
|
|||
selectedFilters: string[];
|
||||
}
|
||||
|
||||
const ExportSummary: React.FC<ExportSummaryProps> = ({ dateRange, selectedFilters }) => {
|
||||
const ExportSummary: React.FC<ExportSummaryProps> = ({
|
||||
dateRange,
|
||||
selectedFilters,
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-sm text-gray-500">
|
||||
{dateRange.from?.toLocaleDateString()} - {dateRange.to?.toLocaleDateString()}
|
||||
{selectedFilters.length > 0 && ` · ${selectedFilters.length} filter${selectedFilters.length > 1 ? "s" : ""}`}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{dateRange.from?.toLocaleDateString()} -{" "}
|
||||
{dateRange.to?.toLocaleDateString()}
|
||||
{selectedFilters.length > 0 &&
|
||||
` · ${selectedFilters.length} filter${selectedFilters.length > 1 ? "s" : ""}`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportSummary;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { Radio } from "antd";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import type { ExportScope, EntityType } from "./types";
|
||||
|
||||
interface ExportTypeSelectorProps {
|
||||
|
|
@ -8,37 +9,58 @@ interface ExportTypeSelectorProps {
|
|||
entityType: EntityType;
|
||||
}
|
||||
|
||||
const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange, entityType }) => {
|
||||
const items: Array<{
|
||||
value: ExportScope;
|
||||
title: (t: EntityType) => string;
|
||||
description: (t: EntityType) => string;
|
||||
}> = [
|
||||
{
|
||||
value: "daily",
|
||||
title: (t) => `Day-by-day breakdown by ${t}`,
|
||||
description: (t) => `Daily metrics for each ${t}`,
|
||||
},
|
||||
{
|
||||
value: "daily_with_keys",
|
||||
title: (t) => `Day-by-day breakdown by ${t} and key`,
|
||||
description: (t) => `Daily metrics for each ${t}, split by API key`,
|
||||
},
|
||||
{
|
||||
value: "daily_with_models",
|
||||
title: (t) => `Day-by-day by ${t} and model`,
|
||||
description: () => `Daily metrics split by model`,
|
||||
},
|
||||
];
|
||||
|
||||
const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
entityType,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 block mb-2">Export type</label>
|
||||
<Radio.Group value={value} onChange={(e) => onChange(e.target.value)} className="w-full">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-start p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
|
||||
<Radio value="daily" className="mt-0.5" />
|
||||
<Label className="text-sm font-medium block mb-2">Export type</Label>
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onValueChange={(v) => onChange(v as ExportScope)}
|
||||
className="w-full space-y-2"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<label
|
||||
key={item.value}
|
||||
className="flex items-start p-3 border border-border rounded-lg hover:bg-muted cursor-pointer transition-colors"
|
||||
>
|
||||
<RadioGroupItem value={item.value} className="mt-0.5" />
|
||||
<div className="ml-3 flex-1">
|
||||
<div className="font-medium text-sm">Day-by-day breakdown by {entityType}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">Daily metrics for each {entityType}</div>
|
||||
<div className="font-medium text-sm">
|
||||
{item.title(entityType)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
{item.description(entityType)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
|
||||
<Radio value="daily_with_keys" className="mt-0.5" />
|
||||
<div className="ml-3 flex-1">
|
||||
<div className="font-medium text-sm">Day-by-day breakdown by {entityType} and key</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">Daily metrics for each {entityType}, split by API key</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
|
||||
<Radio value="daily_with_models" className="mt-0.5" />
|
||||
<div className="ml-3 flex-1">
|
||||
<div className="font-medium text-sm">Day-by-day by {entityType} and model</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">Daily metrics split by model</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue