diff --git a/ui/litellm-dashboard/eslint.config.mjs b/ui/litellm-dashboard/eslint.config.mjs index ad2923bf2a0..37162c9517e 100644 --- a/ui/litellm-dashboard/eslint.config.mjs +++ b/ui/litellm-dashboard/eslint.config.mjs @@ -5,6 +5,7 @@ import prettier from "eslint-config-prettier/flat"; import unusedImports from "eslint-plugin-unused-imports"; import testingLibrary from "eslint-plugin-testing-library"; import jestDom from "eslint-plugin-jest-dom"; +import pluginQuery from "@tanstack/eslint-plugin-query"; import local from "./scripts/eslint-rules/index.mjs"; const eslintConfig = [ @@ -14,10 +15,12 @@ const eslintConfig = [ js.configs.recommended, ...tseslint.configs.recommended, ...nextCoreWebVitals, + ...pluginQuery.configs["flat/recommended"], prettier, { plugins: { "unused-imports": unusedImports, local }, rules: { + "@tanstack/query/exhaustive-deps": ["error", { allowlist: { variables: ["accessToken", "apiClient", "demo"] } }], "unused-imports/no-unused-imports": "error", "local/no-large-inline-object-arg": "warn", "local/no-long-condition-chain": "warn", diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 37027f215fe..0375fcd42aa 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -60,6 +60,7 @@ "@eslint/js": "9.39.2", "@tailwindcss/forms": "0.5.11", "@tailwindcss/postcss": "4.3.2", + "@tanstack/eslint-plugin-query": "5.104.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", @@ -3440,6 +3441,183 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.104.0", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.104.0.tgz", + "integrity": "sha512-WHStlNTKraESaAjeuZ0zJr+t3ZigZgAJ/FQX1sdLGSF+U0vXlHgcAoPqm27VixMRyXaFHCx9xBJiUmPNcxMpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.70.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "^5.6.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@tanstack/pacer": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.21.1.tgz", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 388b6c7eab9..52783635734 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -77,6 +77,7 @@ "@eslint/js": "9.39.2", "@tailwindcss/forms": "0.5.11", "@tailwindcss/postcss": "4.3.2", + "@tanstack/eslint-plugin-query": "5.104.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx index 86b596d4bcd..6f5087faad2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx @@ -40,7 +40,7 @@ export function GuardrailDetail({ guardrailId, onBack, accessToken = null, start error: detailError, } = useGuardrailsUsageDetail(guardrailId, { accessToken, startDate, endDate }); const { data: logsData, isLoading: logsLoading } = useQuery({ - queryKey: ["guardrails-usage-logs", guardrailId, logsPage, logsPageSize], + queryKey: ["guardrails-usage-logs", guardrailId, logsPage, logsPageSize, startDate, endDate], queryFn: () => getGuardrailsUsageLogs(accessToken!, { guardrailId, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerHealth.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerHealth.ts index 9ad8a6f43fa..340531058b8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerHealth.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerHealth.ts @@ -52,7 +52,14 @@ export const useMCPServerHealth = () => { ); return { - ...query, + data: query.data, + error: query.error, + isError: query.isError, + isFetching: query.isFetching, + isLoading: query.isLoading, + isSuccess: query.isSuccess, + refetch: query.refetch, + status: query.status, recheckServerHealth, recheckingServerIds, }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts index 6489bc2171d..1a9bb69254d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts @@ -621,6 +621,30 @@ describe("useSelectedTeamModels", () => { expect(modelAvailableCall).toHaveBeenCalledTimes(1); }); + it("keeps team model results in separate cache entries", async () => { + const teamOneResponse = { data: [{ ...mockProxyModel, id: "team-1-model" }] }; + const teamTwoResponse = { data: [{ ...mockProxyModel, id: "team-2-model" }] }; + (modelAvailableCall as any).mockResolvedValueOnce(teamOneResponse).mockResolvedValueOnce(teamTwoResponse); + + const { result, rerender } = renderHook(({ teamID }: { teamID: string }) => useSelectedTeamModels(teamID), { + wrapper, + initialProps: { teamID: "team-1" }, + }); + + await waitFor(() => expect(result.current.data).toEqual(teamOneResponse)); + + rerender({ teamID: "team-2" }); + + await waitFor(() => expect(result.current.data).toEqual(teamTwoResponse)); + expect(modelAvailableCall).toHaveBeenCalledTimes(2); + expect( + queryClient + .getQueriesData({ queryKey: ["selectedTeamModels", "list"] }) + .map(([, data]) => data?.data[0]?.id) + .sort(), + ).toEqual(["team-1-model", "team-2-model"]); + }); + it("should handle error when modelAvailableCall fails", async () => { const errorMessage = "Failed to fetch team models"; const testError = new Error(errorMessage); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts index b5cc329d4c9..11d8a54412c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts @@ -247,7 +247,12 @@ export const useModelHub = () => { export const useAllProxyModels = () => { const { accessToken, userId, userRole } = useAuthorized(); return useQuery({ - queryKey: allProxyModelsKeys.list({}), + queryKey: allProxyModelsKeys.list({ + filters: { + userId: userId ?? "", + userRole: userRole ?? "", + }, + }), queryFn: async () => await modelAvailableCall(accessToken!, userId!, userRole!, true, null, true, false, "expand"), enabled: Boolean(accessToken && userId && userRole), }); @@ -256,7 +261,12 @@ export const useAllProxyModels = () => { export const useUserModels = (): UseQueryResult => { const { accessToken, userId, userRole } = useAuthorized(); return useQuery({ - queryKey: userModelsKeys.list({}), + queryKey: userModelsKeys.list({ + filters: { + userId: userId ?? "", + userRole: userRole ?? "", + }, + }), queryFn: async () => { const response = await modelAvailableCall(accessToken!, userId!, userRole!); return response["data"].map((model: { id: string }) => model.id); @@ -268,7 +278,13 @@ export const useUserModels = (): UseQueryResult => { export const useSelectedTeamModels = (teamID: string | null) => { const { accessToken, userId, userRole } = useAuthorized(); return useQuery({ - queryKey: selectedTeamModelsKeys.list({}), + queryKey: selectedTeamModelsKeys.list({ + filters: { + teamID: teamID ?? "", + userId: userId ?? "", + userRole: userRole ?? "", + }, + }), queryFn: async () => await modelAvailableCall(accessToken!, userId!, userRole!, true, teamID!), enabled: Boolean(accessToken && userId && userRole && teamID), }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts index 56e45216ec5..2f68aae7323 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts @@ -116,6 +116,36 @@ describe("useTeams", () => { expect(fetchTeams).toHaveBeenCalledTimes(1); }); + it("keeps team list results scoped to the authorized user and role", async () => { + const otherUserTeams = [mockTeams[1]]; + vi.mocked(fetchTeams).mockResolvedValueOnce(mockTeams).mockResolvedValueOnce(otherUserTeams); + + const { result, rerender } = renderHook(() => useTeams(), { wrapper }); + + await waitFor(() => expect(result.current.data).toEqual(mockTeams)); + + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "another-user-id", + userRole: "Internal User", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + rerender(); + + await waitFor(() => expect(result.current.data).toEqual(otherUserTeams)); + expect(fetchTeams).toHaveBeenCalledTimes(2); + expect( + queryClient + .getQueriesData({ queryKey: ["teams", "list"] }) + .map(([, teams]) => teams?.[0]?.team_id) + .sort(), + ).toEqual(["team-1", "team-2"]); + }); + it("should handle error when fetchTeams fails", async () => { const errorMessage = "Failed to fetch teams"; const testError = new Error(errorMessage); @@ -398,7 +428,10 @@ describe("useTeam", () => { }); it("should use initialData from teams list cache when available", async () => { - queryClient.setQueryData(["teams", "list", { params: {} }], mockTeams); + queryClient.setQueryData( + ["teams", "list", { params: { filters: { userId: "test-user-id", userRole: "Admin" } } }], + mockTeams, + ); const { result } = renderHook(() => useTeam("team-1"), { wrapper }); @@ -413,7 +446,10 @@ describe("useTeam", () => { }); it("should return undefined initialData when teamId is not in cache", () => { - queryClient.setQueryData(["teams", "list", { params: {} }], mockTeams); + queryClient.setQueryData( + ["teams", "list", { params: { filters: { userId: "test-user-id", userRole: "Admin" } } }], + mockTeams, + ); const { result } = renderHook(() => useTeam("non-existent-team"), { wrapper }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts index 7d1d035b4d4..a662f7c0725 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts @@ -121,7 +121,12 @@ export const teamKeys = createQueryKeys("teams"); export const useTeams = (): UseQueryResult => { const { accessToken, userId, userRole } = useAuthorized(); return useQuery({ - queryKey: teamKeys.list({}), + queryKey: teamKeys.list({ + filters: { + userId: userId ?? "", + userRole: userRole ?? "", + }, + }), queryFn: async () => await fetchTeams(accessToken!, userId, userRole, null), enabled: Boolean(accessToken), }); @@ -159,7 +164,7 @@ export const useAllTeams = (): UseQueryResult => { }; export const useTeam = (teamId?: string) => { - const { accessToken } = useAuthorized(); + const { accessToken, userId, userRole } = useAuthorized(); const queryClient = useQueryClient(); return useQuery({ queryKey: teamKeys.detail(teamId!), @@ -177,7 +182,14 @@ export const useTeam = (teamId?: string) => { initialData: () => { if (!teamId) return undefined; - const teams = queryClient.getQueryData(teamKeys.list({})); + const teams = queryClient.getQueryData( + teamKeys.list({ + filters: { + userId: userId ?? "", + userRole: userRole ?? "", + }, + }), + ); return teams?.find((team) => team.team_id === teamId); }, @@ -202,6 +214,7 @@ export const useInfiniteTeams = (pageSize: number = 50, search?: string, organiz ...(search && { search }), ...(organizationId && { organizationId }), ...(userId && { userId }), + isAdmin: isAdmin ? "true" : "false", }, }), queryFn: async ({ pageParam }) => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx index a7af5c978f9..ef13f5c7f7a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx @@ -155,7 +155,7 @@ const MCPToolsViewer = ({ error: mcpToolsError, refetch: refetchTools, } = useQuery({ - queryKey: ["mcpTools", serverId, passthroughHeaders, oauthToken], + queryKey: ["mcpTools", serverId, passthroughHeaders, oauthToken, userID], queryFn: async () => { if (!accessToken) throw new Error("Access Token required"); const result = await listMCPTools(accessToken, serverId, buildCustomHeaders()); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx index 5b98ca82c16..4fc234a31ab 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx @@ -244,17 +244,19 @@ const ViewUserDashboard: React.FC = ({ return await userListCall( accessToken, - userIdFilter ? [userIdFilter] : null, - pagination.pageIndex + 1, - pagination.pageSize, + userListQueryFilters.userId ? [userListQueryFilters.userId] : null, + userListQueryFilters.page, + userListQueryFilters.pageSize, null, - userRoleFilter ?? null, - teamFilter ?? null, - ssoUserIdFilter ?? null, - sortBy, - sortOrder, - orgAdminOrgIds ? orgAdminOrgIds.map((o) => o.organization_id) : null, - searchFilter, + userListQueryFilters.role ?? null, + userListQueryFilters.team ?? null, + userListQueryFilters.ssoUserId ?? null, + userListQueryFilters.sortBy, + userListQueryFilters.sortOrder, + userListQueryFilters.orgAdminOrgIds + ? userListQueryFilters.orgAdminOrgIds.map((organization) => organization.organization_id) + : null, + userListQueryFilters.search, ); }, enabled: Boolean(accessToken && token && userRole && userID), diff --git a/ui/litellm-dashboard/src/components/add_model/AutoRouterAvailability.tsx b/ui/litellm-dashboard/src/components/add_model/AutoRouterAvailability.tsx index 135ebb958db..bbc37d0de09 100644 --- a/ui/litellm-dashboard/src/components/add_model/AutoRouterAvailability.tsx +++ b/ui/litellm-dashboard/src/components/add_model/AutoRouterAvailability.tsx @@ -30,7 +30,11 @@ export const useAutoRouterAvailability = (accessToken: string, body: Request, en queryFn: ({ signal }) => apiClient.post("/auto_router/availability", { accessToken, - body: { ...body, complexity_router_config: JSON.parse(debounced) }, + body: { + team_id: body.team_id, + saved_model_id: body.saved_model_id, + complexity_router_config: JSON.parse(debounced), + }, signal, }), enabled: enabled && Boolean(accessToken), @@ -53,8 +57,10 @@ export const useAutoRouterAvailability = (accessToken: string, body: Request, en return query.data.error ?? null; }; return { - ...query, + data: query.data, + isError: query.isError, isPending: query.isPending || (query.isFetching && !query.isFetchedAfterMount), + refetch: query.refetch, isChecking, saveBlockedReason: saveBlockedReason(), }; diff --git a/ui/litellm-dashboard/src/components/chat/LogsPanel.tsx b/ui/litellm-dashboard/src/components/chat/LogsPanel.tsx index e967efde6a4..299404942a6 100644 --- a/ui/litellm-dashboard/src/components/chat/LogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/LogsPanel.tsx @@ -246,20 +246,19 @@ const LogsPanel: React.FC = ({ accessToken, userId }) => { const [page, setPage] = useState(1); const [selectedLog, setSelectedLog] = useState(null); - const startDate = getStartMoment(timeRange).utc().format("YYYY-MM-DD HH:mm:ss"); - const endDate = moment().utc().format("YYYY-MM-DD HH:mm:ss"); - - const logsCallOptions = { - accessToken, - start_date: startDate, - end_date: endDate, - page, - page_size: PAGE_SIZE, - params: { user_id: userId, sort_by: "startTime", sort_order: "desc" as const }, - }; const logsQueryOptions = { queryKey: [LOGS_QUERY_KEY, accessToken, userId, timeRange, page], - queryFn: () => uiSpendLogsCall(logsCallOptions), + queryFn: () => { + const logsCallOptions = { + accessToken, + start_date: getStartMoment(timeRange).utc().format("YYYY-MM-DD HH:mm:ss"), + end_date: moment().utc().format("YYYY-MM-DD HH:mm:ss"), + page, + page_size: PAGE_SIZE, + params: { user_id: userId, sort_by: "startTime", sort_order: "desc" as const }, + }; + return uiSpendLogsCall(logsCallOptions); + }, enabled: !!accessToken && !!userId, placeholderData: keepPreviousData, }; @@ -270,10 +269,14 @@ const LogsPanel: React.FC = ({ accessToken, userId }) => { const totalPages = logs?.total_pages ?? 0; const total = logs?.total ?? 0; - const detailStartDate = selectedLog ? moment(selectedLog.startTime).utc().format("YYYY-MM-DD HH:mm:ss") : ""; const { data: detailData, isLoading: isDetailLoading } = useQuery({ queryKey: [LOGS_QUERY_KEY, "detail", accessToken, selectedLog?.request_id, selectedLog?.startTime], - queryFn: () => uiSpendLogDetailsCall(accessToken, selectedLog!.request_id, detailStartDate), + queryFn: () => + uiSpendLogDetailsCall( + accessToken, + selectedLog!.request_id, + moment(selectedLog!.startTime).utc().format("YYYY-MM-DD HH:mm:ss"), + ), enabled: !!accessToken && !!selectedLog, }); const details = detailData as LogDetails | undefined; diff --git a/ui/litellm-dashboard/src/components/chat/UsagePanel.tsx b/ui/litellm-dashboard/src/components/chat/UsagePanel.tsx index e839890a357..2e50cf69df9 100644 --- a/ui/litellm-dashboard/src/components/chat/UsagePanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/UsagePanel.tsx @@ -91,11 +91,11 @@ const TIME_RANGE_OPTIONS: { value: TimeRange; label: string }[] = [ const UsagePanel: React.FC = ({ accessToken, userId }) => { const [timeRange, setTimeRange] = useState("30d"); - const { start, end } = getDateRange(timeRange); const { data, isLoading } = useQuery({ queryKey: [USAGE_QUERY_KEY, accessToken, userId, timeRange], queryFn: () => { + const { start, end } = getDateRange(timeRange); const request = { accessToken, startTime: start, diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx index ff26e30baca..2a865f7bb1b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx @@ -136,7 +136,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, const { data: selectedKeyInfo } = useQuery(keyInfoQueryOptions); const urlLogQueryOptions: UseQueryOptions = { - queryKey: ["logs", "byId", urlLogId, accessToken], + queryKey: ["logs", "byId", urlLogId, accessToken, startTime, endTime, isCustomDate], queryFn: async () => { if (urlLogId === null) return null; const window = formatLogsWindow(startTime, endTime, isCustomDate); diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx index baf713b1537..a4579daf37d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx @@ -161,6 +161,9 @@ describe("useLogFilterLogic", () => { ["startTime", { startTime: "2025-02-02T00:00:00" }], ["columnFilters", { columnFilters: [{ id: LOG_FILTER_IDS.TEAM_ID, value: "team-2" }] }], ["excludeInternalHealthChecks", { excludeInternalHealthChecks: true }], + ["token", { token: "other-token" }], + ["userRole", { userRole: "Internal User" }], + ["userID", { userID: "user-2" }], ])("refetches when %s changes", async (_label, nextProps) => { const { rerender } = renderHook((props: HookOverrides) => useLogFilterLogic({ ...defaultProps, ...props }), { wrapper, diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index 8b2d9f22c15..e5183925a4b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -155,6 +155,9 @@ export function useLogFilterLogic({ sortOrder, excludeInternalHealthChecks, sessionCursor, + token, + userRole, + userID, ], queryFn: async () => { if (!accessToken || !token || !userRole || !userID) {