mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge pull request #19918 from BerriAI/litellm_ui_spend_logs_store
[Feature] UI - Spend Logs: Settings Modal
This commit is contained in:
commit
054918e7a3
5 changed files with 554 additions and 6 deletions
|
|
@ -0,0 +1,63 @@
|
|||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import useAuthorized from "../useAuthorized";
|
||||
|
||||
export interface StoreRequestInSpendLogsParams {
|
||||
store_prompts_in_spend_logs: boolean;
|
||||
maximum_spend_logs_retention_period?: string;
|
||||
}
|
||||
|
||||
export interface StoreRequestInSpendLogsResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const performStoreRequestInSpendLogs = async (
|
||||
accessToken: string,
|
||||
params: StoreRequestInSpendLogsParams
|
||||
): Promise<StoreRequestInSpendLogsResponse> => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/config/update` : `/config/update`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
general_settings: {
|
||||
store_prompts_in_spend_logs: params.store_prompts_in_spend_logs,
|
||||
...(params.maximum_spend_logs_retention_period && {
|
||||
maximum_spend_logs_retention_period: params.maximum_spend_logs_retention_period,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage =
|
||||
errorData?.error?.message || errorData?.message || errorData?.detail || "Failed to update spend logs settings";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useStoreRequestInSpendLogs = (): UseMutationResult<
|
||||
StoreRequestInSpendLogsResponse,
|
||||
Error,
|
||||
StoreRequestInSpendLogsParams
|
||||
> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useMutation<StoreRequestInSpendLogsResponse, Error, StoreRequestInSpendLogsParams>({
|
||||
mutationFn: async (params: StoreRequestInSpendLogsParams) => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return await performStoreRequestInSpendLogs(accessToken, params);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -7,6 +7,22 @@ import { describe, it, expect } from "vitest";
|
|||
import { getAvailablePages } from "./page_utils";
|
||||
import { menuGroups } from "./leftnav";
|
||||
import { pageDescriptions } from "./page_metadata";
|
||||
import { internalUserRoles } from "@/utils/roles";
|
||||
|
||||
/**
|
||||
* Check if a page is accessible to internal users
|
||||
* A page is accessible if:
|
||||
* 1. It has no role restrictions, OR
|
||||
* 2. Its roles include at least one internal user role
|
||||
*/
|
||||
const isPageAccessibleToInternalUsers = (pageRoles?: string[]): boolean => {
|
||||
if (!pageRoles || pageRoles.length === 0) {
|
||||
return true; // No role restrictions
|
||||
}
|
||||
|
||||
// Check if any of the page's roles match internal user roles
|
||||
return pageRoles.some(role => internalUserRoles.includes(role));
|
||||
};
|
||||
|
||||
describe("Page Utils - LeftNav Sync", () => {
|
||||
it("should return all pages from leftnav configuration", () => {
|
||||
|
|
@ -32,26 +48,32 @@ describe("Page Utils - LeftNav Sync", () => {
|
|||
const availablePages = getAvailablePages();
|
||||
const availablePageKeys = availablePages.map((p) => p.page);
|
||||
|
||||
// Collect all page keys from menuGroups (excluding parent containers)
|
||||
// Collect all page keys from menuGroups (excluding parent containers and pages not accessible to internal users)
|
||||
const menuPageKeys: string[] = [];
|
||||
const excludedParents = ["tools", "experimental", "settings"];
|
||||
|
||||
menuGroups.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
if (item.page && !excludedParents.includes(item.page)) {
|
||||
if (
|
||||
item.page &&
|
||||
!excludedParents.includes(item.page) &&
|
||||
isPageAccessibleToInternalUsers(item.roles)
|
||||
) {
|
||||
menuPageKeys.push(item.page);
|
||||
}
|
||||
|
||||
// Add children
|
||||
// Add children (only if accessible to internal users)
|
||||
if (item.children) {
|
||||
item.children.forEach((child) => {
|
||||
menuPageKeys.push(child.page);
|
||||
if (isPageAccessibleToInternalUsers(child.roles)) {
|
||||
menuPageKeys.push(child.page);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Every menu page should be in available pages
|
||||
// Every menu page accessible to internal users should be in available pages
|
||||
menuPageKeys.forEach((pageKey) => {
|
||||
expect(
|
||||
availablePageKeys,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,348 @@
|
|||
import { useStoreRequestInSpendLogs } from "@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
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";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs");
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
fromBackend: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/components/shared/errorUtils", () => ({
|
||||
parseErrorMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseStoreRequestInSpendLogs = vi.mocked(useStoreRequestInSpendLogs);
|
||||
const mockNotificationsManager = vi.mocked(NotificationsManager);
|
||||
const mockParseErrorMessage = vi.mocked(parseErrorMessage);
|
||||
|
||||
describe("SpendLogsSettingsModal", () => {
|
||||
const mockOnCancel = vi.fn();
|
||||
const mockOnSuccess = vi.fn();
|
||||
const mockMutateAsync = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
isVisible: true,
|
||||
onCancel: mockOnCancel,
|
||||
onSuccess: mockOnSuccess,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseStoreRequestInSpendLogs.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
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} />);
|
||||
|
||||
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} />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
expect(switchElement).not.toBeChecked();
|
||||
|
||||
await user.click(switchElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update retention period input", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
|
||||
await user.type(retentionInput, "30d");
|
||||
|
||||
expect(retentionInput).toHaveValue("30d");
|
||||
});
|
||||
|
||||
it("should submit form with store prompts enabled and retention period", async () => {
|
||||
const user = userEvent.setup();
|
||||
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");
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{
|
||||
store_prompts_in_spend_logs: true,
|
||||
maximum_spend_logs_retention_period: "30d",
|
||||
},
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should submit form with store prompts disabled and no retention period", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{
|
||||
store_prompts_in_spend_logs: false,
|
||||
maximum_spend_logs_retention_period: undefined,
|
||||
},
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show success notification and call onSuccess on successful submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Spend logs settings updated successfully");
|
||||
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification when submission fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Network error");
|
||||
mockMutateAsync.mockRejectedValue(error);
|
||||
mockParseErrorMessage.mockReturnValue("Network error");
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification from onError callback", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Backend error");
|
||||
mockMutateAsync.mockImplementation((params, options) => {
|
||||
options?.onError?.(error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
mockParseErrorMessage.mockReturnValue("Backend error");
|
||||
|
||||
renderWithProviders(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable cancel button when pending", () => {
|
||||
mockUseStoreRequestInSpendLogs.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
} 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 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 reset form fields after successful submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
const { rerender } = 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 saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
rerender(<SpendLogsSettingsModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const updatedSwitchElement = screen.getByRole("switch");
|
||||
const updatedRetentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
|
||||
expect(updatedSwitchElement).not.toBeChecked();
|
||||
expect(updatedRetentionInput).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call onSuccess when it is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
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 submit form with only store prompts enabled and no retention period", async () => {
|
||||
const user = userEvent.setup();
|
||||
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 saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{
|
||||
store_prompts_in_spend_logs: true,
|
||||
maximum_spend_logs_retention_period: undefined,
|
||||
},
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
"use client";
|
||||
|
||||
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, Space, Switch } from "antd";
|
||||
import React 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 storePromptsValue = Form.useWatch('store_prompts_in_spend_logs', form);
|
||||
|
||||
const handleFormSubmit = async (formValues: StoreRequestInSpendLogsParams) => {
|
||||
try {
|
||||
await mutateAsync(formValues, {
|
||||
onSuccess: () => {
|
||||
NotificationsManager.success("Spend logs settings updated successfully");
|
||||
form.resetFields();
|
||||
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="Spend Logs Settings"
|
||||
open={isVisible}
|
||||
width={600}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={handleCancel} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" loading={isPending} onClick={() => form.submit()}>
|
||||
{isPending ? "Saving..." : "Save Settings"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
labelCol={{ flex: "auto", style: { textAlign: "left" } }}
|
||||
wrapperCol={{ flex: "auto", style: { textAlign: "right" } }}
|
||||
onFinish={handleFormSubmit}
|
||||
initialValues={{
|
||||
store_prompts_in_spend_logs: false,
|
||||
maximum_spend_logs_retention_period: undefined,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="store_prompts_in_spend_logs"
|
||||
tooltip="When enabled, prompts will be stored in spend logs for tracking and analysis purposes."
|
||||
valuePropName="checked"
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>Store Prompts in Spend Logs</span>
|
||||
<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="Set the maximum retention period for spend logs (e.g., '7d' for 7 days, '30d' for 30 days). Leave empty for no limit."
|
||||
labelCol={{ flex: "auto", style: { textAlign: "left" } }}
|
||||
wrapperCol={{ flex: "0 0 25%", style: { textAlign: "right" } }}
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g., 7d, 30d"
|
||||
prefix={<ClockCircleOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpendLogsSettingsModal;
|
||||
|
|
@ -12,7 +12,7 @@ import { RequestResponsePanel } from "./RequestResponsePanel";
|
|||
import { ErrorViewer } from "./ErrorViewer";
|
||||
import { internalUserRoles } from "../../utils/roles";
|
||||
import { ConfigInfoMessage } from "./ConfigInfoMessage";
|
||||
import { Tooltip } from "antd";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import KeyInfoView from "../templates/key_info_view";
|
||||
import { SessionView } from "./SessionView";
|
||||
|
|
@ -31,6 +31,8 @@ import { truncateString } from "@/utils/textUtils";
|
|||
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
|
||||
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
|
||||
import NewBadge from "../common_components/NewBadge";
|
||||
import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsModal";
|
||||
import { SettingOutlined } from "@ant-design/icons";
|
||||
|
||||
interface SpendLogsTableProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -91,6 +93,7 @@ export default function SpendLogsTable({
|
|||
|
||||
const [expandedRequestId, setExpandedRequestId] = useState<string | null>(null);
|
||||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
|
||||
const [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
|
@ -526,6 +529,13 @@ export default function SpendLogsTable({
|
|||
"Request Logs"
|
||||
)}
|
||||
</h1>
|
||||
{!selectedSessionId && (
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => setIsSpendLogsSettingsModalVisible(true)}
|
||||
title="Spend Logs Settings"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? (
|
||||
<KeyInfoView
|
||||
|
|
@ -552,6 +562,11 @@ 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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue