mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): migrate view_logs page + modals to shadcn
Migrates the page-level chrome of the view_logs subtree: - index.tsx: Tremor TabGroup/Tab/TabList/TabPanels/TabPanel -> shadcn Tabs/TabsList/TabsTrigger/TabsContent; Tremor Switch -> shadcn Switch; antd Button/Tag/Tooltip -> shadcn Button/Badge/Tooltip. Filter dropdowns, custom date range inputs, and pagination controls remain unchanged. The inline RequestViewer component (used by the legacy expanded-row view) also migrates its Tooltip/Tag usages. - audit_logs.tsx: antd Table -> DataTable (shadcn Table driven by @tanstack/react-table) with ColumnDef<AuditLogEntry>[]; antd Input/Select/Button/Pagination/Spin -> shadcn Input/Select/Button + custom prev/next pagination per BLUEPRINT. Action palette (green/ blue/red/orange for created/updated/deleted/rotated) kept raw. - SpendLogsSettingsModal.tsx: antd Form.useForm -> react-hook-form with FormProvider + Controller; antd Form.Item tooltip -> shadcn Tooltip around a lucide HelpCircle; antd Switch -> shadcn Switch; antd-coupled button-class test (.ant-btn-loading) replaced with semantic data-loading attribute; skeleton selector updated to match shadcn's .animate-pulse class. Adds index.tsx and audit_logs.tsx to the .eslintrc.json override list (they keep categorical status palettes for log-level action badges). Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
d3708b967f
commit
889d12f9b9
5 changed files with 498 additions and 284 deletions
|
|
@ -100,6 +100,8 @@
|
|||
"src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx",
|
||||
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx",
|
||||
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx",
|
||||
"src/components/view_logs/index.tsx",
|
||||
"src/components/view_logs/audit_logs.tsx",
|
||||
"src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx",
|
||||
"src/components/permissions/VectorStorePermissions.tsx",
|
||||
"src/components/permissions/AgentPermissions.tsx",
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ describe("SpendLogsSettingsModal", () => {
|
|||
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
expect(saveButton).toHaveAttribute("data-loading", "true");
|
||||
});
|
||||
|
||||
it("should show loading state on save button when deleting field", () => {
|
||||
|
|
@ -294,7 +294,7 @@ describe("SpendLogsSettingsModal", () => {
|
|||
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
expect(saveButton).toHaveAttribute("data-loading", "true");
|
||||
});
|
||||
|
||||
it("should call onCancel when cancel button is clicked after modifying form", async () => {
|
||||
|
|
@ -422,8 +422,8 @@ describe("SpendLogsSettingsModal", () => {
|
|||
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText("e.g., 7d, 30d")).not.toBeInTheDocument();
|
||||
|
||||
// Check for skeleton elements (Ant Design Skeleton.Input renders with ant-skeleton class)
|
||||
const skeletons = document.querySelectorAll(".ant-skeleton");
|
||||
// Check for shadcn Skeleton elements — they apply "animate-pulse" as part of the base class.
|
||||
const skeletons = document.querySelectorAll(".animate-pulse");
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { ConfigType, GeneralSettingsFieldName, useDeleteProxyConfigField, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
|
||||
import { StoreRequestInSpendLogsParams, useStoreRequestInSpendLogs } from "@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs";
|
||||
import {
|
||||
ConfigType,
|
||||
GeneralSettingsFieldName,
|
||||
useDeleteProxyConfigField,
|
||||
useProxyConfig,
|
||||
} from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
|
||||
import {
|
||||
StoreRequestInSpendLogsParams,
|
||||
useStoreRequestInSpendLogs,
|
||||
} from "@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { parseErrorMessage } from "@/components/shared/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -12,11 +20,19 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Clock } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form, Switch } from "antd";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Clock, HelpCircle, LoaderCircle } from "lucide-react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm, FormProvider, Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
interface SpendLogsSettingsModalProps {
|
||||
isVisible: boolean;
|
||||
|
|
@ -24,41 +40,66 @@ interface SpendLogsSettingsModalProps {
|
|||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const SpendLogsSettingsModal: React.FC<SpendLogsSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
interface FormValues {
|
||||
store_prompts_in_spend_logs: boolean;
|
||||
maximum_spend_logs_retention_period?: string;
|
||||
}
|
||||
|
||||
const STORE_PROMPTS_TOOLTIP_FALLBACK =
|
||||
"When enabled, prompts will be stored in spend logs for tracking and analysis purposes.";
|
||||
const RETENTION_PERIOD_TOOLTIP_FALLBACK =
|
||||
"Set the maximum retention period for spend logs (e.g., '7d' for 7 days, '30d' for 30 days). Leave empty for no limit.";
|
||||
|
||||
const SpendLogsSettingsModal: React.FC<SpendLogsSettingsModalProps> = ({
|
||||
isVisible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { mutateAsync, isPending } = useStoreRequestInSpendLogs();
|
||||
const { mutateAsync: deleteField, isPending: isDeletingField } = useDeleteProxyConfigField();
|
||||
const { data: proxyConfigData, isLoading: isLoadingConfig, refetch } = useProxyConfig(ConfigType.GENERAL_SETTINGS);
|
||||
const storePromptsValue = Form.useWatch('store_prompts_in_spend_logs', form);
|
||||
const {
|
||||
data: proxyConfigData,
|
||||
isLoading: isLoadingConfig,
|
||||
refetch,
|
||||
} = useProxyConfig(ConfigType.GENERAL_SETTINGS);
|
||||
|
||||
// Refetch config when modal opens to ensure we have the latest values
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
store_prompts_in_spend_logs: false,
|
||||
maximum_spend_logs_retention_period: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Reapply backend-provided defaults whenever the modal opens or the config changes.
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
refetch();
|
||||
}
|
||||
if (!isVisible) return;
|
||||
refetch();
|
||||
}, [isVisible, refetch]);
|
||||
|
||||
// Compute initial values from fetched config data
|
||||
const initialValues = useMemo(() => {
|
||||
if (!proxyConfigData) {
|
||||
return {
|
||||
store_prompts_in_spend_logs: false,
|
||||
maximum_spend_logs_retention_period: undefined,
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!proxyConfigData) return;
|
||||
const storePromptsField = proxyConfigData.find(
|
||||
(field) => field.field_name === "store_prompts_in_spend_logs",
|
||||
);
|
||||
const retentionPeriodField = proxyConfigData.find(
|
||||
(field) => field.field_name === "maximum_spend_logs_retention_period",
|
||||
);
|
||||
form.reset({
|
||||
store_prompts_in_spend_logs: Boolean(storePromptsField?.field_value),
|
||||
maximum_spend_logs_retention_period:
|
||||
(retentionPeriodField?.field_value as string | undefined) ?? "",
|
||||
});
|
||||
}, [proxyConfigData, form]);
|
||||
|
||||
const storePromptsField = proxyConfigData.find(field => field.field_name === 'store_prompts_in_spend_logs');
|
||||
const retentionPeriodField = proxyConfigData.find(field => field.field_name === 'maximum_spend_logs_retention_period');
|
||||
const storePromptsDescription =
|
||||
proxyConfigData?.find((f) => f.field_name === "store_prompts_in_spend_logs")?.field_description ||
|
||||
STORE_PROMPTS_TOOLTIP_FALLBACK;
|
||||
const retentionPeriodDescription =
|
||||
proxyConfigData?.find((f) => f.field_name === "maximum_spend_logs_retention_period")
|
||||
?.field_description || RETENTION_PERIOD_TOOLTIP_FALLBACK;
|
||||
|
||||
return {
|
||||
store_prompts_in_spend_logs: storePromptsField?.field_value ?? false,
|
||||
maximum_spend_logs_retention_period: retentionPeriodField?.field_value ?? undefined,
|
||||
};
|
||||
}, [proxyConfigData]);
|
||||
|
||||
const handleFormSubmit = async (formValues: StoreRequestInSpendLogsParams) => {
|
||||
const handleFormSubmit = async (formValues: FormValues) => {
|
||||
try {
|
||||
// If maximum_spend_logs_retention_period is empty/null, delete the field first
|
||||
const retentionPeriodValue = formValues.maximum_spend_logs_retention_period;
|
||||
const shouldDeleteRetentionPeriod =
|
||||
!retentionPeriodValue ||
|
||||
|
|
@ -72,121 +113,89 @@ const SpendLogsSettingsModal: React.FC<SpendLogsSettingsModalProps> = ({ isVisib
|
|||
});
|
||||
} catch (deleteError) {
|
||||
// If field doesn't exist, that's okay - continue with update
|
||||
console.warn("Failed to delete retention period field (may not exist):", deleteError);
|
||||
console.warn(
|
||||
"Failed to delete retention period field (may not exist):",
|
||||
deleteError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the settings (excluding maximum_spend_logs_retention_period if it's empty)
|
||||
const updateParams: StoreRequestInSpendLogsParams = {
|
||||
store_prompts_in_spend_logs: formValues.store_prompts_in_spend_logs,
|
||||
...(retentionPeriodValue &&
|
||||
typeof retentionPeriodValue === "string" &&
|
||||
retentionPeriodValue.trim() !== "" && {
|
||||
maximum_spend_logs_retention_period: retentionPeriodValue,
|
||||
}),
|
||||
maximum_spend_logs_retention_period: retentionPeriodValue,
|
||||
}),
|
||||
};
|
||||
|
||||
await mutateAsync(updateParams, {
|
||||
onSuccess: () => {
|
||||
NotificationsManager.success("Spend logs settings updated successfully");
|
||||
refetch(); // Refetch config to get updated values
|
||||
refetch();
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationsManager.fromBackend("Failed to save spend logs settings: " + parseErrorMessage(error));
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to save spend logs settings: " + parseErrorMessage(error),
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to save spend logs settings: " + parseErrorMessage(error));
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to save spend logs settings: " + parseErrorMessage(error),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
form.reset();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const isSubmitting = isPending || isDeletingField;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isVisible}
|
||||
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
|
||||
>
|
||||
<Dialog open={isVisible} onOpenChange={(o) => (!o ? handleCancel() : undefined)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-semibold">
|
||||
Spend Logs Settings
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form
|
||||
key={proxyConfigData ? JSON.stringify(initialValues) : "loading"}
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
onFinish={handleFormSubmit}
|
||||
initialValues={initialValues}
|
||||
>
|
||||
<Form.Item
|
||||
label="Store Prompts in Spend Logs"
|
||||
name="store_prompts_in_spend_logs"
|
||||
tooltip={
|
||||
proxyConfigData?.find(
|
||||
(f) => f.field_name === "store_prompts_in_spend_logs",
|
||||
)?.field_description ||
|
||||
"When enabled, prompts will be stored in spend logs for tracking and analysis purposes."
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<div>
|
||||
{isLoadingConfig ? (
|
||||
<Skeleton className="h-6 w-full" />
|
||||
) : (
|
||||
<Switch
|
||||
checked={storePromptsValue ?? false}
|
||||
onChange={(checked) =>
|
||||
form.setFieldValue(
|
||||
"store_prompts_in_spend_logs",
|
||||
checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Maximum Spend Logs Retention Period (Optional)"
|
||||
name="maximum_spend_logs_retention_period"
|
||||
tooltip={
|
||||
proxyConfigData?.find(
|
||||
(f) =>
|
||||
f.field_name ===
|
||||
"maximum_spend_logs_retention_period",
|
||||
)?.field_description ||
|
||||
"Set the maximum retention period for spend logs (e.g., '7d' for 7 days, '30d' for 30 days). Leave empty for no limit."
|
||||
}
|
||||
>
|
||||
{isLoadingConfig ? (
|
||||
<Skeleton className="h-6 w-full" />
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
|
||||
<Input placeholder="e.g., 7d, 30d" className="pl-8" />
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-4">
|
||||
<StorePromptsField
|
||||
isLoading={isLoadingConfig}
|
||||
description={storePromptsDescription}
|
||||
/>
|
||||
<RetentionPeriodField
|
||||
isLoading={isLoadingConfig}
|
||||
description={retentionPeriodDescription}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={isPending || isDeletingField || isLoadingConfig}
|
||||
disabled={isSubmitting || isLoadingConfig}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => form.submit()}
|
||||
disabled={isPending || isDeletingField || isLoadingConfig}
|
||||
onClick={form.handleSubmit(handleFormSubmit)}
|
||||
disabled={isSubmitting || isLoadingConfig}
|
||||
data-loading={isSubmitting || undefined}
|
||||
>
|
||||
{isPending || isDeletingField ? "Saving..." : "Save Settings"}
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Save Settings"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -194,4 +203,93 @@ const SpendLogsSettingsModal: React.FC<SpendLogsSettingsModalProps> = ({ isVisib
|
|||
);
|
||||
};
|
||||
|
||||
function FieldTooltip({ text }: { text: string }) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="More info"
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs text-xs">{text}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function StorePromptsField({
|
||||
isLoading,
|
||||
description,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
description: string;
|
||||
}) {
|
||||
const { control } = useFormContext<FormValues>();
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<Label htmlFor="store-prompts-switch" className="text-sm font-medium">
|
||||
Store Prompts in Spend Logs
|
||||
</Label>
|
||||
<FieldTooltip text={description} />
|
||||
</div>
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-6 w-12" />
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
name="store_prompts_in_spend_logs"
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
id="store-prompts-switch"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RetentionPeriodField({
|
||||
isLoading,
|
||||
description,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
description: string;
|
||||
}) {
|
||||
const { register } = useFormContext<FormValues>();
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Label htmlFor="retention-period-input" className="text-sm font-medium">
|
||||
Maximum Spend Logs Retention Period (Optional)
|
||||
</Label>
|
||||
<FieldTooltip text={description} />
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-9 w-full" />
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
id="retention-period-input"
|
||||
placeholder="e.g., 7d, 30d"
|
||||
className="pl-8"
|
||||
{...register("maximum_spend_logs_retention_period")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpendLogsSettingsModal;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
import { useState } from "react";
|
||||
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||
import { Table, Tag, Input, Select, Button, Pagination, Spin } from "antd";
|
||||
import { RefreshCcw as ReloadOutlined, Loader2 as LoadingOutlined } from "lucide-react";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import moment from "moment";
|
||||
import { RefreshCcw as ReloadOutlined, LoaderCircle, Search as SearchIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { uiAuditLogsCall } from "../networking";
|
||||
import { AuditLogEntry } from "./columns";
|
||||
import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer";
|
||||
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
|
||||
|
||||
const { Search } = Input;
|
||||
import { DataTable } from "./table";
|
||||
|
||||
interface AuditLogsProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -31,15 +40,78 @@ const TABLE_NAME_DISPLAY: Record<string, string> = {
|
|||
LiteLLM_ProxyModelTable: "Models",
|
||||
};
|
||||
|
||||
const ACTION_COLOR: Record<string, string> = {
|
||||
created: "green",
|
||||
updated: "blue",
|
||||
deleted: "red",
|
||||
rotated: "orange",
|
||||
/** Per-action palette — categorical; kept raw via eslint override. */
|
||||
const ACTION_BADGE_CLASS: Record<string, string> = {
|
||||
created: "bg-green-100 text-green-700 hover:bg-green-100",
|
||||
updated: "bg-blue-100 text-blue-700 hover:bg-blue-100",
|
||||
deleted: "bg-red-100 text-red-700 hover:bg-red-100",
|
||||
rotated: "bg-orange-100 text-orange-700 hover:bg-orange-100",
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: "All Actions", value: "__all__" },
|
||||
{ label: "Created", value: "created" },
|
||||
{ label: "Updated", value: "updated" },
|
||||
{ label: "Deleted", value: "deleted" },
|
||||
{ label: "Rotated", value: "rotated" },
|
||||
];
|
||||
|
||||
const TABLE_OPTIONS = [
|
||||
{ label: "All Tables", value: "__all__" },
|
||||
{ label: "Keys", value: "LiteLLM_VerificationToken" },
|
||||
{ label: "Teams", value: "LiteLLM_TeamTable" },
|
||||
{ label: "Users", value: "LiteLLM_UserTable" },
|
||||
{ label: "Organizations", value: "LiteLLM_OrganizationTable" },
|
||||
{ label: "Models", value: "LiteLLM_ProxyModelTable" },
|
||||
];
|
||||
|
||||
/** Small debounced-on-enter/clear text input used for filter fields. */
|
||||
function FilterSearchInput({
|
||||
placeholder,
|
||||
width,
|
||||
onSubmit,
|
||||
onClear,
|
||||
}: {
|
||||
placeholder: string;
|
||||
width: number;
|
||||
onSubmit: (val: string) => void;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<div className="relative" style={{ width }}>
|
||||
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
if (!e.target.value) onClear();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onSubmit(value);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
className="pl-8 pr-8 h-9 text-sm"
|
||||
/>
|
||||
{value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue("");
|
||||
onClear();
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-sm"
|
||||
aria-label="Clear"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuditLogs({
|
||||
userID,
|
||||
userRole,
|
||||
|
|
@ -105,85 +177,75 @@ export default function AuditLogs({
|
|||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<AuditLogEntry> = [
|
||||
const columns: ColumnDef<AuditLogEntry>[] = [
|
||||
{
|
||||
title: "Timestamp",
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
width: 200,
|
||||
render: (val: string) => (
|
||||
header: "Timestamp",
|
||||
accessorKey: "updated_at",
|
||||
cell: (info) => (
|
||||
<span className="font-mono text-xs whitespace-nowrap">
|
||||
{moment.utc(val).local().format("MMM D, YYYY HH:mm:ss")}
|
||||
{moment.utc(info.getValue() as string).local().format("MMM D, YYYY HH:mm:ss")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Action",
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (val: string) => (
|
||||
<Tag color={ACTION_COLOR[val] ?? "default"} className="capitalize">
|
||||
{val}
|
||||
</Tag>
|
||||
),
|
||||
header: "Action",
|
||||
accessorKey: "action",
|
||||
cell: (info) => {
|
||||
const val = info.getValue() as string;
|
||||
return (
|
||||
<Badge className={`capitalize ${ACTION_BADGE_CLASS[val] ?? "bg-muted text-foreground hover:bg-muted"}`}>
|
||||
{val}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Table",
|
||||
dataIndex: "table_name",
|
||||
key: "table_name",
|
||||
width: 130,
|
||||
render: (val: string) => TABLE_NAME_DISPLAY[val] ?? val,
|
||||
header: "Table",
|
||||
accessorKey: "table_name",
|
||||
cell: (info) => {
|
||||
const val = info.getValue() as string;
|
||||
return <span>{TABLE_NAME_DISPLAY[val] ?? val}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Object ID",
|
||||
dataIndex: "object_id",
|
||||
key: "object_id",
|
||||
render: (val: string) => (
|
||||
<span className="font-mono text-xs">{val}</span>
|
||||
),
|
||||
header: "Object ID",
|
||||
accessorKey: "object_id",
|
||||
cell: (info) => <span className="font-mono text-xs">{info.getValue() as string}</span>,
|
||||
},
|
||||
{
|
||||
title: "Changed By",
|
||||
dataIndex: "changed_by",
|
||||
key: "changed_by",
|
||||
width: 200,
|
||||
render: (val: string) => <DefaultProxyAdminTag userId={val} />,
|
||||
header: "Changed By",
|
||||
accessorKey: "changed_by",
|
||||
cell: (info) => <DefaultProxyAdminTag userId={info.getValue() as string} />,
|
||||
},
|
||||
{
|
||||
title: "API Key (Hash)",
|
||||
dataIndex: "changed_by_api_key",
|
||||
key: "changed_by_api_key",
|
||||
width: 140,
|
||||
render: (val: string) =>
|
||||
val ? (
|
||||
header: "API Key (Hash)",
|
||||
accessorKey: "changed_by_api_key",
|
||||
cell: (info) => {
|
||||
const val = info.getValue() as string;
|
||||
return val ? (
|
||||
<span className="font-mono text-xs">{val.slice(0, 12)}…</span>
|
||||
) : (
|
||||
"—"
|
||||
),
|
||||
<span>—</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!premiumUser) {
|
||||
return (
|
||||
<div style={{ textAlign: "center", marginTop: "20px" }}>
|
||||
<h1 style={{ display: "block", marginBottom: "10px" }}>✨ Enterprise Feature.</h1>
|
||||
<p style={{ display: "block", marginBottom: "10px" }}>
|
||||
<div className="text-center mt-5">
|
||||
<h1 className="block mb-2.5 text-xl font-semibold">✨ Enterprise Feature.</h1>
|
||||
<p className="block mb-2.5">
|
||||
This is a LiteLLM Enterprise feature, and requires a valid key to use.
|
||||
</p>
|
||||
<p style={{ display: "block", marginBottom: "20px", fontStyle: "italic" }}>
|
||||
<p className="block mb-5 italic text-muted-foreground">
|
||||
Here's a preview of what Audit Logs offer:
|
||||
</p>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={auditLogsPreviewImg}
|
||||
alt="Audit Logs Preview"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "700px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
className="max-w-full max-h-[700px] rounded-lg shadow-md mx-auto"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
|
|
@ -194,10 +256,11 @@ export default function AuditLogs({
|
|||
|
||||
const auditLogs: AuditLogEntry[] = query.data?.audit_logs ?? [];
|
||||
const total: number = query.data?.total ?? 0;
|
||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="bg-background rounded-lg shadow">
|
||||
{/* Header */}
|
||||
<div className="border-b px-6 py-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
|
@ -206,96 +269,125 @@ export default function AuditLogs({
|
|||
|
||||
{/* Filters + pagination on same row */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Search
|
||||
<FilterSearchInput
|
||||
placeholder="Object ID"
|
||||
allowClear
|
||||
style={{ width: 200 }}
|
||||
onSearch={(val) => { setObjectId(val); resetPage(); }}
|
||||
onChange={(e) => { if (!e.target.value) { setObjectId(""); resetPage(); } }}
|
||||
width={200}
|
||||
onSubmit={(val) => { setObjectId(val); resetPage(); }}
|
||||
onClear={() => { setObjectId(""); resetPage(); }}
|
||||
/>
|
||||
<Search
|
||||
<FilterSearchInput
|
||||
placeholder="Changed By"
|
||||
allowClear
|
||||
style={{ width: 180 }}
|
||||
onSearch={(val) => { setChangedBy(val); resetPage(); }}
|
||||
onChange={(e) => { if (!e.target.value) { setChangedBy(""); resetPage(); } }}
|
||||
width={180}
|
||||
onSubmit={(val) => { setChangedBy(val); resetPage(); }}
|
||||
onClear={() => { setChangedBy(""); resetPage(); }}
|
||||
/>
|
||||
<Search
|
||||
<FilterSearchInput
|
||||
placeholder="Team ID"
|
||||
allowClear
|
||||
style={{ width: 180 }}
|
||||
onSearch={(val) => { setTeamId(val); resetPage(); }}
|
||||
onChange={(e) => { if (!e.target.value) { setTeamId(""); resetPage(); } }}
|
||||
width={180}
|
||||
onSubmit={(val) => { setTeamId(val); resetPage(); }}
|
||||
onClear={() => { setTeamId(""); resetPage(); }}
|
||||
/>
|
||||
<Search
|
||||
<FilterSearchInput
|
||||
placeholder="Key Hash"
|
||||
allowClear
|
||||
style={{ width: 180 }}
|
||||
onSearch={(val) => { setKeyHash(val); resetPage(); }}
|
||||
onChange={(e) => { if (!e.target.value) { setKeyHash(""); resetPage(); } }}
|
||||
width={180}
|
||||
onSubmit={(val) => { setKeyHash(val); resetPage(); }}
|
||||
onClear={() => { setKeyHash(""); resetPage(); }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All Actions"
|
||||
allowClear
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ label: "Created", value: "created" },
|
||||
{ label: "Updated", value: "updated" },
|
||||
{ label: "Deleted", value: "deleted" },
|
||||
{ label: "Rotated", value: "rotated" },
|
||||
]}
|
||||
onChange={(val) => { setAction(val); resetPage(); }}
|
||||
/>
|
||||
value={action ?? "__all__"}
|
||||
onValueChange={(val) => {
|
||||
setAction(val === "__all__" ? undefined : val);
|
||||
resetPage();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-[140px] text-sm">
|
||||
<SelectValue placeholder="All Actions" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="All Tables"
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
options={[
|
||||
{ label: "Keys", value: "LiteLLM_VerificationToken" },
|
||||
{ label: "Teams", value: "LiteLLM_TeamTable" },
|
||||
{ label: "Users", value: "LiteLLM_UserTable" },
|
||||
{ label: "Organizations", value: "LiteLLM_OrganizationTable" },
|
||||
{ label: "Models", value: "LiteLLM_ProxyModelTable" },
|
||||
]}
|
||||
onChange={(val) => { setTableName(val); resetPage(); }}
|
||||
/>
|
||||
value={tableName ?? "__all__"}
|
||||
onValueChange={(val) => {
|
||||
setTableName(val === "__all__" ? undefined : val);
|
||||
resetPage();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-[150px] text-sm">
|
||||
<SelectValue placeholder="All Tables" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TABLE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Pagination + refresh pushed to the right */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
icon={<ReloadOutlined className="animate-spin" />}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => query.refetch()}
|
||||
disabled={query.isFetching}
|
||||
/>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
showTotal={(t) => `${t} total`}
|
||||
showSizeChanger={false}
|
||||
size="small"
|
||||
onChange={(p) => setPage(p)}
|
||||
/>
|
||||
aria-label="Refresh"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<ReloadOutlined className={`h-4 w-4 ${query.isFetching ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<span className="text-muted-foreground whitespace-nowrap">{total} total</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="h-8"
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<span className="whitespace-nowrap">
|
||||
Page {page} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="h-8"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table — pagination handled in header */}
|
||||
<Table<AuditLogEntry>
|
||||
columns={columns}
|
||||
dataSource={auditLogs}
|
||||
rowKey="id"
|
||||
loading={{
|
||||
spinning: query.isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined className="animate-spin" />} size="small" />,
|
||||
}}
|
||||
size="small"
|
||||
pagination={false}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
style: { cursor: "pointer" },
|
||||
})}
|
||||
/>
|
||||
{query.isLoading ? (
|
||||
<div className="p-6 space-y-2">
|
||||
<Skeleton className="h-9 w-full" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
<div className="flex items-center justify-center pt-2 text-muted-foreground gap-2">
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm">Loading audit logs…</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DataTable<AuditLogEntry, unknown>
|
||||
columns={columns}
|
||||
data={auditLogs}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AuditLogDrawer
|
||||
|
|
|
|||
|
|
@ -6,9 +6,16 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
|
|||
import { truncateString } from "@/utils/textUtils";
|
||||
import { Settings as SettingOutlined, RefreshCcw as SyncOutlined } from "lucide-react";
|
||||
import { Row } from "@tanstack/react-table";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { Switch, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import { Button, Tag, Tooltip } from "antd";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { internalUserRoles } from "../../utils/roles";
|
||||
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
|
||||
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
|
||||
|
|
@ -156,8 +163,8 @@ export default function SpendLogsTable({
|
|||
const LiveTailControls = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">Live Tail</span>
|
||||
<Switch color="green" checked={isLiveTail} defaultChecked={true} onChange={setIsLiveTail} />
|
||||
<span className="text-sm font-medium text-foreground">Live Tail</span>
|
||||
<Switch checked={isLiveTail} onCheckedChange={setIsLiveTail} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -472,22 +479,27 @@ export default function SpendLogsTable({
|
|||
|
||||
return (
|
||||
<div className="w-full max-w-screen p-6 overflow-x-hidden box-border">
|
||||
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "request logs" : "audit logs")}>
|
||||
<TabList>
|
||||
<Tab>Request Logs</Tab>
|
||||
<Tab>Audit Logs</Tab>
|
||||
<Tab>Deleted Keys</Tab>
|
||||
<Tab>Deleted Teams</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Tabs
|
||||
defaultValue="request-logs"
|
||||
onValueChange={(val) => setActiveTab(val === "request-logs" ? "request logs" : val === "audit-logs" ? "audit logs" : val)}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="request-logs">Request Logs</TabsTrigger>
|
||||
<TabsTrigger value="audit-logs">Audit Logs</TabsTrigger>
|
||||
<TabsTrigger value="deleted-keys">Deleted Keys</TabsTrigger>
|
||||
<TabsTrigger value="deleted-teams">Deleted Teams</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="request-logs">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold">Request Logs</h1>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsSpendLogsSettingsModalVisible(true)}
|
||||
title="Spend Logs Settings"
|
||||
/>
|
||||
>
|
||||
<SettingOutlined className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? (
|
||||
<KeyInfoView
|
||||
|
|
@ -522,7 +534,7 @@ export default function SpendLogsTable({
|
|||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<svg
|
||||
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
|
||||
className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -593,12 +605,13 @@ export default function SpendLogsTable({
|
|||
<LiveTailControls />
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
icon={<SyncOutlined className={isButtonLoading ? "animate-spin" : undefined} />}
|
||||
variant="outline"
|
||||
onClick={handleRefresh}
|
||||
disabled={isButtonLoading}
|
||||
title="Fetch data"
|
||||
className="h-9"
|
||||
>
|
||||
<SyncOutlined className={`mr-2 h-4 w-4 ${isButtonLoading ? "animate-spin" : ""}`} />
|
||||
{isButtonLoading ? "Fetching" : "Fetch"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -616,7 +629,7 @@ export default function SpendLogsTable({
|
|||
className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-gray-500">to</span>
|
||||
<span className="text-muted-foreground">to</span>
|
||||
<div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
|
|
@ -633,7 +646,7 @@ export default function SpendLogsTable({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-700 whitespace-nowrap">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
Showing {logs.isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0} -{" "}
|
||||
{logs.isLoading
|
||||
? "..."
|
||||
|
|
@ -643,7 +656,7 @@ export default function SpendLogsTable({
|
|||
of {logs.isLoading ? "..." : filteredLogs ? filteredLogs.total : 0} results
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-700 min-w-[90px]">
|
||||
<span className="text-sm text-muted-foreground min-w-[90px]">
|
||||
Page {logs.isLoading ? "..." : currentPage} of{" "}
|
||||
{logs.isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1}
|
||||
</span>
|
||||
|
|
@ -695,21 +708,20 @@ export default function SpendLogsTable({
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<AuditLogs
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
isActive={activeTab === "audit logs"}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel><DeletedKeysPage /></TabPanel>
|
||||
<TabPanel><DeletedTeamsPage /></TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="audit-logs">
|
||||
<AuditLogs
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
isActive={activeTab === "audit logs"}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="deleted-keys"><DeletedKeysPage /></TabsContent>
|
||||
<TabsContent value="deleted-teams"><DeletedTeamsPage /></TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Log Details Drawer */}
|
||||
<LogDetailsDrawer
|
||||
|
|
@ -823,9 +835,14 @@ export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onO
|
|||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Request ID:</span>
|
||||
{row.original.request_id.length > 64 ? (
|
||||
<Tooltip title={row.original.request_id}>
|
||||
<span className="font-mono text-sm">{truncatedRequestId}</span>
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="font-mono text-sm">{truncatedRequestId}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{row.original.request_id}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<span className="font-mono text-sm">{row.original.request_id}</span>
|
||||
)}
|
||||
|
|
@ -848,9 +865,14 @@ export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onO
|
|||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">API Base:</span>
|
||||
<Tooltip title={row.original.api_base || "-"}>
|
||||
<span className="max-w-[15ch] truncate block">{row.original.api_base || "-"}</span>
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="max-w-[15ch] truncate block">{row.original.api_base || "-"}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{row.original.api_base || "-"}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{row?.original?.requester_ip_address && (
|
||||
<div className="flex">
|
||||
|
|
@ -936,7 +958,7 @@ export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onO
|
|||
{row.original.metadata?.attempted_retries !== undefined && row.original.metadata?.attempted_retries !== null
|
||||
? row.original.metadata.attempted_retries > 0
|
||||
? `${row.original.metadata.attempted_retries}${row.original.metadata.max_retries !== undefined && row.original.metadata.max_retries !== null ? ` / ${row.original.metadata.max_retries}` : ''}`
|
||||
: <Tag color="green">None</Tag>
|
||||
: <Badge className="bg-green-100 text-green-700 hover:bg-green-100">None</Badge>
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue