mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-18 02:17:35 +00:00
Move "Store Prompts in Spend Logs" toggle to Admin Settings
Previously, the "Store Prompts in Spend Logs" and "Maximum Spend Logs Retention Period" settings were surfaced via a gear-icon modal on the Logs page. The gear was visible to every authenticated user even though the backend endpoints (/config/update, /config/list) require PROXY_ADMIN — so non-admins could open the modal but the request would 403 on load and save, giving a confusing UX. Move the controls into a new "Logging Settings" tab under Admin Settings, which is already gated to admins at the sidebar. Remove the gear button and the onOpenSettings prop chain (ConfigInfoMessage → LogDetailContent → LogDetailsDrawer). ConfigInfoMessage now points users to "Admin Settings → Logging Settings" inline.
This commit is contained in:
@@ -21,6 +21,7 @@ import { useBaseUrl } from "./constants";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { addAllowedIP, deleteAllowedIP, getAllowedIPs, getSSOSettings } from "./networking";
|
||||
import SCIMConfig from "./SCIM";
|
||||
import LoggingSettings from "./Settings/AdminSettings/LoggingSettings/LoggingSettings";
|
||||
import SSOSettings from "./Settings/AdminSettings/SSOSettings/SSOSettings";
|
||||
import UISettings from "./Settings/AdminSettings/UISettings/UISettings";
|
||||
import HashicorpVault from "./Settings/AdminSettings/HashicorpVault/HashicorpVault";
|
||||
@@ -362,6 +363,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
|
||||
),
|
||||
children: <UISettings />,
|
||||
},
|
||||
{
|
||||
key: "logging-settings",
|
||||
label: "Logging Settings",
|
||||
children: <LoggingSettings />,
|
||||
},
|
||||
{
|
||||
key: "hashicorp-vault",
|
||||
label: "Hashicorp Vault",
|
||||
|
||||
+61
-194
@@ -5,11 +5,20 @@ import { parseErrorMessage } from "@/components/shared/errorUtils";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../../tests/test-utils";
|
||||
import SpendLogsSettingsModal from "./SpendLogsSettingsModal";
|
||||
import { renderWithProviders } from "../../../../../tests/test-utils";
|
||||
import LoggingSettings from "./LoggingSettings";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs");
|
||||
vi.mock("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig");
|
||||
vi.mock("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig")>(
|
||||
"@/app/(dashboard)/hooks/proxyConfig/useProxyConfig",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
useProxyConfig: vi.fn(),
|
||||
useDeleteProxyConfigField: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
@@ -26,19 +35,11 @@ const mockUseDeleteProxyConfigField = vi.mocked(useDeleteProxyConfigField);
|
||||
const mockNotificationsManager = vi.mocked(NotificationsManager);
|
||||
const mockParseErrorMessage = vi.mocked(parseErrorMessage);
|
||||
|
||||
describe("SpendLogsSettingsModal", () => {
|
||||
const mockOnCancel = vi.fn();
|
||||
const mockOnSuccess = vi.fn();
|
||||
describe("LoggingSettings", () => {
|
||||
const mockMutateAsync = vi.fn();
|
||||
const mockDeleteField = vi.fn();
|
||||
const mockRefetch = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
isVisible: true,
|
||||
onCancel: mockOnCancel,
|
||||
onSuccess: mockOnSuccess,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseStoreRequestInSpendLogs.mockReturnValue({
|
||||
@@ -57,50 +58,19 @@ describe("SpendLogsSettingsModal", () => {
|
||||
mockParseErrorMessage.mockImplementation((error: any) => error?.message || String(error));
|
||||
});
|
||||
|
||||
it("should render the modal", () => {
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByText("Spend Logs Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render form fields with initial values", () => {
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
it("should render the card with title and form fields", () => {
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
expect(screen.getByText("Logging Settings")).toBeInTheDocument();
|
||||
expect(screen.getByText("Store Prompts in Spend Logs")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Maximum Spend Logs Retention Period (Optional)")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("e.g., 7d, 30d")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render cancel and save buttons", () => {
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Save Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onCancel when cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onCancel when modal close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const closeButton = screen.getByRole("button", { name: /close/i });
|
||||
await user.click(closeButton);
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should toggle store prompts switch", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
expect(switchElement).not.toBeChecked();
|
||||
@@ -114,7 +84,7 @@ describe("SpendLogsSettingsModal", () => {
|
||||
|
||||
it("should update retention period input", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
|
||||
await user.type(retentionInput, "30d");
|
||||
@@ -124,13 +94,13 @@ describe("SpendLogsSettingsModal", () => {
|
||||
|
||||
it("should submit form with store prompts enabled and retention period", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
mockMutateAsync.mockImplementation(async (_params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
await user.click(switchElement);
|
||||
@@ -148,21 +118,21 @@ describe("SpendLogsSettingsModal", () => {
|
||||
store_prompts_in_spend_logs: true,
|
||||
maximum_spend_logs_retention_period: "30d",
|
||||
},
|
||||
expect.any(Object)
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should submit form with store prompts disabled and no retention period", async () => {
|
||||
it("should delete retention period field when left empty on submit", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
mockMutateAsync.mockImplementation(async (_params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
@@ -173,207 +143,106 @@ describe("SpendLogsSettingsModal", () => {
|
||||
{
|
||||
store_prompts_in_spend_logs: false,
|
||||
},
|
||||
expect.any(Object)
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show success notification and call onSuccess on successful submission", async () => {
|
||||
it("should show success notification on successful submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
mockMutateAsync.mockImplementation(async (_params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Spend logs settings updated successfully");
|
||||
expect(mockRefetch).toHaveBeenCalled();
|
||||
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification when submission fails", async () => {
|
||||
it("should show error notification when submission throws", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Network error");
|
||||
mockMutateAsync.mockRejectedValue(error);
|
||||
mockParseErrorMessage.mockReturnValue("Network error");
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save spend logs settings: Network error");
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
"Failed to save spend logs settings: Network error",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification from onError callback", async () => {
|
||||
it("should show error notification via onError callback", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Backend error");
|
||||
mockMutateAsync.mockImplementation((params, options) => {
|
||||
mockMutateAsync.mockImplementation((_params, options) => {
|
||||
options?.onError?.(error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
mockParseErrorMessage.mockReturnValue("Backend error");
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save spend logs settings: Backend error");
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
"Failed to save spend logs settings: Backend error",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable cancel button when pending", () => {
|
||||
it("should show loading state on save button when update pending", () => {
|
||||
mockUseStoreRequestInSpendLogs.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
});
|
||||
|
||||
it("should disable cancel button when deleting field", () => {
|
||||
it("should show loading state on save button when delete pending", () => {
|
||||
mockUseDeleteProxyConfigField.mockReturnValue({
|
||||
mutateAsync: mockDeleteField,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
});
|
||||
|
||||
it("should disable cancel button when loading config", () => {
|
||||
it("should disable save button while config is loading", () => {
|
||||
mockUseProxyConfig.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should show loading state on save button when pending", () => {
|
||||
mockUseStoreRequestInSpendLogs.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
});
|
||||
|
||||
it("should show loading state on save button when deleting field", () => {
|
||||
mockUseDeleteProxyConfigField.mockReturnValue({
|
||||
mutateAsync: mockDeleteField,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
});
|
||||
|
||||
it("should call onCancel when cancel button is clicked after modifying form", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
await user.click(switchElement);
|
||||
|
||||
const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
|
||||
await user.type(retentionInput, "30d");
|
||||
|
||||
expect(switchElement).toBeChecked();
|
||||
expect(retentionInput).toHaveValue("30d");
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call refetch after successful submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
await user.click(switchElement);
|
||||
|
||||
const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
|
||||
await user.type(retentionInput, "30d");
|
||||
|
||||
expect(switchElement).toBeChecked();
|
||||
expect(retentionInput).toHaveValue("30d");
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalled();
|
||||
expect(mockRefetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call onSuccess when it is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal isVisible={true} onCancel={mockOnCancel} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render modal when isVisible is false", () => {
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} isVisible={false} />);
|
||||
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call refetch when modal opens", () => {
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
expect(mockRefetch).toHaveBeenCalledTimes(1);
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should render form with initial values from config data", () => {
|
||||
@@ -400,7 +269,7 @@ describe("SpendLogsSettingsModal", () => {
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
|
||||
@@ -416,13 +285,11 @@ describe("SpendLogsSettingsModal", () => {
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
// Check that switch and input are not present when loading (skeletons are shown instead)
|
||||
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");
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -431,13 +298,13 @@ describe("SpendLogsSettingsModal", () => {
|
||||
const user = userEvent.setup();
|
||||
const deleteError = new Error("Field does not exist");
|
||||
mockDeleteField.mockRejectedValue(deleteError);
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
mockMutateAsync.mockImplementation(async (_params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
@@ -448,22 +315,22 @@ describe("SpendLogsSettingsModal", () => {
|
||||
{
|
||||
store_prompts_in_spend_logs: false,
|
||||
},
|
||||
expect.any(Object)
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should submit form with only store prompts enabled and no retention period", async () => {
|
||||
it("should submit with only store prompts enabled when retention is empty", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
mockMutateAsync.mockImplementation(async (_params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
renderWithProviders(<LoggingSettings />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
await user.click(switchElement);
|
||||
@@ -477,7 +344,7 @@ describe("SpendLogsSettingsModal", () => {
|
||||
{
|
||||
store_prompts_in_spend_logs: true,
|
||||
},
|
||||
expect.any(Object)
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
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 { ClockCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Form, Input, Skeleton, Space, Switch, Typography } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
const LoggingSettings: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const { mutateAsync, isPending } = useStoreRequestInSpendLogs();
|
||||
const { mutateAsync: deleteField, isPending: isDeletingField } = useDeleteProxyConfigField();
|
||||
const { data: proxyConfigData, isLoading: isLoadingConfig } = useProxyConfig(ConfigType.GENERAL_SETTINGS);
|
||||
const storePromptsValue = Form.useWatch("store_prompts_in_spend_logs", form);
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
if (!proxyConfigData) {
|
||||
return {
|
||||
store_prompts_in_spend_logs: false,
|
||||
maximum_spend_logs_retention_period: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
);
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const retentionPeriodValue = formValues.maximum_spend_logs_retention_period;
|
||||
const shouldDeleteRetentionPeriod =
|
||||
!retentionPeriodValue ||
|
||||
(typeof retentionPeriodValue === "string" && retentionPeriodValue.trim() === "");
|
||||
|
||||
if (shouldDeleteRetentionPeriod) {
|
||||
try {
|
||||
await deleteField({
|
||||
config_type: ConfigType.GENERAL_SETTINGS,
|
||||
field_name: GeneralSettingsFieldName.MAXIMUM_SPEND_LOGS_RETENTION_PERIOD,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
console.warn("Failed to delete retention period field (may not exist):", deleteError);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
};
|
||||
|
||||
await mutateAsync(updateParams, {
|
||||
onSuccess: () => {
|
||||
NotificationsManager.success("Spend logs settings updated successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationsManager.fromBackend("Failed to save spend logs settings: " + parseErrorMessage(error));
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to save spend logs settings: " + parseErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Logging Settings">
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Typography.Paragraph style={{ marginBottom: 0 }} type="secondary">
|
||||
Proxy-wide settings that control how request and response data are written to spend logs.
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Form
|
||||
key={proxyConfigData ? JSON.stringify(initialValues) : "loading"}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
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"
|
||||
>
|
||||
{isLoadingConfig ? (
|
||||
<Skeleton.Input active block />
|
||||
) : (
|
||||
<Switch
|
||||
checked={storePromptsValue ?? false}
|
||||
onChange={(checked) => form.setFieldValue("store_prompts_in_spend_logs", checked)}
|
||||
/>
|
||||
)}
|
||||
</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.Input active block />
|
||||
) : (
|
||||
<Input placeholder="e.g., 7d, 30d" prefix={<ClockCircleOutlined />} />
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isPending || isDeletingField}
|
||||
disabled={isLoadingConfig}
|
||||
>
|
||||
{isPending || isDeletingField ? "Saving..." : "Save Settings"}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoggingSettings;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ConfigInfoMessage } from "./ConfigInfoMessage";
|
||||
|
||||
describe("ConfigInfoMessage", () => {
|
||||
@@ -19,23 +18,8 @@ describe("ConfigInfoMessage", () => {
|
||||
expect(screen.getByText(/store_prompts_in_spend_logs: true/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the settings button when onOpenSettings is provided", () => {
|
||||
render(<ConfigInfoMessage show={true} onOpenSettings={() => {}} />);
|
||||
expect(screen.getByText("open the settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render the settings button when onOpenSettings is omitted", () => {
|
||||
it("should reference Admin Settings \u2192 Logging Settings", () => {
|
||||
render(<ConfigInfoMessage show={true} />);
|
||||
expect(screen.queryByText("open the settings")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onOpenSettings when the settings button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenSettings = vi.fn();
|
||||
|
||||
render(<ConfigInfoMessage show={true} onOpenSettings={onOpenSettings} />);
|
||||
await user.click(screen.getByText("open the settings"));
|
||||
|
||||
expect(onOpenSettings).toHaveBeenCalledOnce();
|
||||
expect(screen.getByText(/Admin Settings → Logging Settings/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,9 @@ import React from "react";
|
||||
|
||||
interface ConfigInfoMessageProps {
|
||||
show: boolean;
|
||||
onOpenSettings?: () => void;
|
||||
}
|
||||
|
||||
export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show, onOpenSettings }) => {
|
||||
export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show }) => {
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
@@ -31,18 +30,8 @@ export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show, onOp
|
||||
<h4 className="text-sm font-medium text-blue-800">Request/Response Data Not Available</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
To view request and response details, enable prompt storage in your LiteLLM configuration by adding the
|
||||
following to your <code className="bg-blue-100 px-1 py-0.5 rounded">proxy_config.yaml</code> file
|
||||
{onOpenSettings && (
|
||||
<> or{" "}
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
className="text-blue-600 hover:text-blue-800 underline font-medium"
|
||||
>
|
||||
open the settings
|
||||
</button>
|
||||
{" "}to configure this directly.
|
||||
</>
|
||||
)}
|
||||
following to your <code className="bg-blue-100 px-1 py-0.5 rounded">proxy_config.yaml</code> file, or toggle
|
||||
the setting in <strong>Admin Settings → Logging Settings</strong>.
|
||||
</p>
|
||||
<pre className="mt-2 bg-white p-3 rounded border border-blue-200 text-xs font-mono overflow-auto">
|
||||
{`general_settings:
|
||||
|
||||
-21
@@ -171,27 +171,6 @@ describe("LogDetailContent", () => {
|
||||
expect(screen.queryByText("Request/Response Data Not Available")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onOpenSettings when user clicks open settings in ConfigInfoMessage", async () => {
|
||||
const onOpenSettings = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<LogDetailContent
|
||||
logEntry={createLogEntry({
|
||||
messages: [],
|
||||
response: {},
|
||||
metadata: {},
|
||||
})}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>,
|
||||
);
|
||||
|
||||
const settingsButton = screen.getByRole("button", { name: /open the settings/i });
|
||||
await user.click(settingsButton);
|
||||
|
||||
expect(onOpenSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should display loading state when isLoadingDetails is true", () => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
|
||||
@@ -37,7 +37,6 @@ const { Text } = Typography;
|
||||
|
||||
export interface LogDetailContentProps {
|
||||
logEntry: LogEntry;
|
||||
onOpenSettings?: () => void;
|
||||
/** When true, log details (messages/response) are still being lazy-loaded. */
|
||||
isLoadingDetails?: boolean;
|
||||
accessToken?: string | null;
|
||||
@@ -51,7 +50,7 @@ export interface LogDetailContentProps {
|
||||
* Designed to be placed inside LogDetailsDrawer's right panel so it can
|
||||
* be reused for both single-log and session-mode views.
|
||||
*/
|
||||
export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails = false, accessToken }: LogDetailContentProps) {
|
||||
export function LogDetailContent({ logEntry, isLoadingDetails = false, accessToken }: LogDetailContentProps) {
|
||||
const metadata = logEntry.metadata || {};
|
||||
const hasError = metadata.status === "failure";
|
||||
const errorInfo = hasError ? metadata.error_information : null;
|
||||
@@ -153,7 +152,7 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
|
||||
{/* Configuration Info Message */}
|
||||
{missingData && (
|
||||
<div className="mb-6">
|
||||
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />
|
||||
<ConfigInfoMessage show={missingData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface LogDetailsDrawerProps {
|
||||
logEntry: LogEntry | null;
|
||||
sessionId?: string | null;
|
||||
accessToken?: string | null;
|
||||
onOpenSettings?: () => void;
|
||||
allLogs?: LogEntry[];
|
||||
onSelectLog?: (log: LogEntry) => void;
|
||||
startTime?: string;
|
||||
@@ -109,7 +108,6 @@ export function LogDetailsDrawer({
|
||||
logEntry,
|
||||
sessionId,
|
||||
accessToken,
|
||||
onOpenSettings,
|
||||
allLogs = [],
|
||||
onSelectLog,
|
||||
startTime,
|
||||
@@ -399,7 +397,6 @@ export function LogDetailsDrawer({
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<LogDetailContent
|
||||
logEntry={enrichedLog}
|
||||
onOpenSettings={onOpenSettings}
|
||||
isLoadingDetails={isLoadingDetails}
|
||||
accessToken={accessToken ?? null}
|
||||
/>
|
||||
|
||||
-156
@@ -1,156 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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 { ClockCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, Form, Input, Modal, Skeleton, Space, Switch, Typography } from "antd";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
interface SpendLogsSettingsModalProps {
|
||||
isVisible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const SpendLogsSettingsModal: React.FC<SpendLogsSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
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);
|
||||
|
||||
// Refetch config when modal opens to ensure we have the latest values
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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) => {
|
||||
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 ||
|
||||
(typeof retentionPeriodValue === "string" && retentionPeriodValue.trim() === "");
|
||||
|
||||
if (shouldDeleteRetentionPeriod) {
|
||||
try {
|
||||
await deleteField({
|
||||
config_type: ConfigType.GENERAL_SETTINGS,
|
||||
field_name: GeneralSettingsFieldName.MAXIMUM_SPEND_LOGS_RETENTION_PERIOD,
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}),
|
||||
};
|
||||
|
||||
await mutateAsync(updateParams, {
|
||||
onSuccess: () => {
|
||||
NotificationsManager.success("Spend logs settings updated successfully");
|
||||
refetch(); // Refetch config to get updated values
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationsManager.fromBackend("Failed to save spend logs settings: " + parseErrorMessage(error));
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to save spend logs settings: " + parseErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<Typography.Title level={5}>Spend Logs Settings</Typography.Title>}
|
||||
open={isVisible}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={handleCancel} disabled={isPending || isDeletingField || isLoadingConfig}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" loading={isPending || isDeletingField} disabled={isLoadingConfig} onClick={() => form.submit()}>
|
||||
{isPending || isDeletingField ? "Saving..." : "Save Settings"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
|
||||
<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.Input active block /> : <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.Input active block /> : <Input
|
||||
placeholder="e.g., 7d, 30d"
|
||||
prefix={<ClockCircleOutlined />}
|
||||
/>}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpendLogsSettingsModal;
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useDeferredValue, useEffect, useRef, useState } from "reac
|
||||
import GuardrailViewer from "@/components/view_logs/GuardrailViewer/GuardrailViewer";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { truncateString } from "@/utils/textUtils";
|
||||
import { SettingOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Row } from "@tanstack/react-table";
|
||||
import { Switch, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import { Button, Tag, Tooltip } from "antd";
|
||||
@@ -28,7 +28,6 @@ import { useLogFilterLogic } from "./log_filter_logic";
|
||||
import { LogDetailsDrawer } from "./LogDetailsDrawer";
|
||||
import { getTimeRangeDisplay } from "./logs_utils";
|
||||
import { RequestResponsePanel } from "./RequestResponsePanel";
|
||||
import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsModal";
|
||||
import { DataTable } from "./table";
|
||||
import { VectorStoreViewer } from "./VectorStoreViewer";
|
||||
|
||||
@@ -85,7 +84,6 @@ export default function SpendLogsTable({
|
||||
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null);
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
|
||||
const [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false);
|
||||
|
||||
const [sortBy, setSortBy] = useState<LogsSortField>("startTime");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
@@ -490,11 +488,6 @@ export default function SpendLogsTable({
|
||||
<TabPanel>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold">Request Logs</h1>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => setIsSpendLogsSettingsModalVisible(true)}
|
||||
title="Spend Logs Settings"
|
||||
/>
|
||||
</div>
|
||||
{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? (
|
||||
<KeyInfoView
|
||||
@@ -511,11 +504,6 @@ export default function SpendLogsTable({
|
||||
onApplyFilters={handleFilterChange}
|
||||
onResetFilters={handleFilterReset}
|
||||
/>
|
||||
<SpendLogsSettingsModal
|
||||
isVisible={isSpendLogsSettingsModalVisible}
|
||||
onCancel={() => setIsSpendLogsSettingsModalVisible(false)}
|
||||
onSuccess={() => setIsSpendLogsSettingsModalVisible(false)}
|
||||
/>
|
||||
<div className="bg-white rounded-lg shadow w-full max-w-full box-border">
|
||||
<div className="border-b px-6 py-4 w-full max-w-full box-border">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-4 md:space-y-0 w-full max-w-full box-border">
|
||||
@@ -725,7 +713,6 @@ export default function SpendLogsTable({
|
||||
logEntry={selectedLog}
|
||||
sessionId={selectedSessionId}
|
||||
accessToken={accessToken}
|
||||
onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)}
|
||||
allLogs={filteredData}
|
||||
onSelectLog={handleSelectLog}
|
||||
startTime={moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")}
|
||||
@@ -734,7 +721,7 @@ export default function SpendLogsTable({
|
||||
);
|
||||
}
|
||||
|
||||
export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onOpenSettings?: () => void }) {
|
||||
export function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||
// Helper function to clean metadata by removing specific fields
|
||||
const formatData = (input: any) => {
|
||||
if (typeof input === "string") {
|
||||
@@ -961,7 +948,7 @@ export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onO
|
||||
/>
|
||||
|
||||
{/* Configuration Info Message - Show when data is missing */}
|
||||
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />
|
||||
<ConfigInfoMessage show={missingData} />
|
||||
|
||||
{/* Request/Response Panel */}
|
||||
<div className="w-full max-w-full overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user