diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 0a5955259..961a32284 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -55,6 +55,10 @@ "filters": { "user": "User", "provider": "Provider", + "searchUser": "Search users...", + "searchProvider": "Search providers...", + "noUserFound": "No matching users found", + "noProviderFound": "No matching providers found", "model": "Model", "endpoint": "Endpoint", "status": "Status", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 1e5863b68..08645e35c 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -55,6 +55,10 @@ "filters": { "user": "ユーザー", "provider": "プロバイダー", + "searchUser": "ユーザーを検索...", + "searchProvider": "プロバイダーを検索...", + "noUserFound": "一致するユーザーが見つかりません", + "noProviderFound": "一致するプロバイダーが見つかりません", "model": "モデル", "endpoint": "エンドポイント", "status": "ステータス", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 04f40b578..c76b733e3 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -55,6 +55,10 @@ "filters": { "user": "Пользователь", "provider": "Поставщик", + "searchUser": "Поиск пользователей...", + "searchProvider": "Поиск провайдеров...", + "noUserFound": "Пользователи не найдены", + "noProviderFound": "Провайдеры не найдены", "model": "Модель", "endpoint": "Эндпоинт", "status": "Статус", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 199d27f6a..025bf3f52 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -55,6 +55,10 @@ "filters": { "user": "用户", "provider": "供应商", + "searchUser": "搜索用户...", + "searchProvider": "搜索供应商...", + "noUserFound": "未找到匹配的用户", + "noProviderFound": "未找到匹配的供应商", "model": "模型", "endpoint": "端点", "status": "状态", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index c0d3de757..09b0970d1 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -55,6 +55,10 @@ "filters": { "user": "使用者", "provider": "供應商", + "searchUser": "搜尋使用者...", + "searchProvider": "搜尋供應商...", + "noUserFound": "未找到匹配的使用者", + "noProviderFound": "未找到匹配的供應商", "model": "模型", "endpoint": "端點", "status": "狀態", diff --git a/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx b/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx index 18fea0449..e49c594b4 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx @@ -289,7 +289,7 @@ export function KeyEditSection({ if (!normalizedKeyProviderGroup) return []; return normalizedKeyProviderGroup.split(",").filter(Boolean); }, [normalizedKeyProviderGroup]); - const extraKeyGroupOption = useMemo(() => { + const _extraKeyGroupOption = useMemo(() => { if (!normalizedKeyProviderGroup) return null; if (normalizedKeyProviderGroup === normalizedUserProviderGroup) return null; if (userGroups.includes(normalizedKeyProviderGroup)) return null; diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx index 9848168b7..e49af3fb8 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx @@ -1,7 +1,7 @@ "use client"; import { addDays, format, parse } from "date-fns"; -import { Download } from "lucide-react"; +import { Check, ChevronsUpDown, Download } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -9,8 +9,17 @@ import { toast } from "sonner"; import { getKeys } from "@/actions/keys"; import { exportUsageLogs } from "@/actions/usage-logs"; import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, @@ -99,6 +108,8 @@ export function UsageLogsFilters({ const [keys, setKeys] = useState(initialKeys); const [localFilters, setLocalFilters] = useState(filters); const [isExporting, setIsExporting] = useState(false); + const [userPopoverOpen, setUserPopoverOpen] = useState(false); + const [providerPopoverOpen, setProviderPopoverOpen] = useState(false); useEffect(() => { if (initialKeys.length > 0) { @@ -263,26 +274,72 @@ export function UsageLogsFilters({ {isAdmin && (
- + + + + + e.stopPropagation()} + onTouchMove={(e) => e.stopPropagation()} + > + + + + + {isUsersLoading ? t("logs.stats.loading") : t("logs.filters.noUserFound")} + + + { + void handleUserChange(""); + setUserPopoverOpen(false); + }} + className="cursor-pointer" + > + {t("logs.filters.allUsers")} + {!localFilters.userId && } + + {users.map((user) => ( + { + void handleUserChange(user.id.toString()); + setUserPopoverOpen(false); + }} + className="cursor-pointer" + > + {user.name} + {localFilters.userId === user.id && ( + + )} + + ))} + + + + +
)} @@ -324,31 +381,82 @@ export function UsageLogsFilters({ {isAdmin && (
- + + + + + e.stopPropagation()} + onTouchMove={(e) => e.stopPropagation()} + > + + + + + {isProvidersLoading + ? t("logs.stats.loading") + : t("logs.filters.noProviderFound")} + + + { + setLocalFilters({ + ...localFilters, + providerId: undefined, + }); + setProviderPopoverOpen(false); + }} + className="cursor-pointer" + > + {t("logs.filters.allProviders")} + {!localFilters.providerId && } + + {providers.map((provider) => ( + { + setLocalFilters({ + ...localFilters, + providerId: provider.id, + }); + setProviderPopoverOpen(false); + }} + className="cursor-pointer" + > + {provider.name} + {localFilters.providerId === provider.id && ( + + )} + + ))} + + + + +
)} diff --git a/src/repository/usage-logs.ts b/src/repository/usage-logs.ts index acaf02d47..845f492bb 100644 --- a/src/repository/usage-logs.ts +++ b/src/repository/usage-logs.ts @@ -172,9 +172,8 @@ export async function findUsageLogsBatch( // Cursor-based pagination: WHERE (created_at, id) < (cursor_created_at, cursor_id) // Using row value comparison for efficient keyset pagination if (cursor) { - const cursorDate = new Date(cursor.createdAt); conditions.push( - sql`(${messageRequest.createdAt}, ${messageRequest.id}) < (${cursorDate.toISOString()}::timestamptz, ${cursor.id})` + sql`(${messageRequest.createdAt}, ${messageRequest.id}) < (${cursor.createdAt}::timestamptz, ${cursor.id})` ); } @@ -185,6 +184,7 @@ export async function findUsageLogsBatch( .select({ id: messageRequest.id, createdAt: messageRequest.createdAt, + createdAtRaw: sql`to_char(${messageRequest.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`, sessionId: messageRequest.sessionId, requestSequence: messageRequest.requestSequence, userName: users.name, @@ -228,9 +228,7 @@ export async function findUsageLogsBatch( // Calculate next cursor from the last record const lastLog = logsToReturn[logsToReturn.length - 1]; const nextCursor = - hasMore && lastLog?.createdAt - ? { createdAt: lastLog.createdAt.toISOString(), id: lastLog.id } - : null; + hasMore && lastLog?.createdAtRaw ? { createdAt: lastLog.createdAtRaw, id: lastLog.id } : null; const logs: UsageLogRow[] = logsToReturn.map((row) => { const totalRowTokens = diff --git a/src/repository/user.ts b/src/repository/user.ts index 47c617958..392d70665 100644 --- a/src/repository/user.ts +++ b/src/repository/user.ts @@ -167,9 +167,8 @@ export async function findUserListBatch( // Cursor-based pagination: WHERE (created_at, id) > (cursor_created_at, cursor_id) if (cursor) { - const cursorDate = new Date(cursor.createdAt); conditions.push( - sql`(${users.createdAt}, ${users.id}) > (${cursorDate.toISOString()}::timestamptz, ${cursor.id})` + sql`(${users.createdAt}, ${users.id}) > (${cursor.createdAt}::timestamptz, ${cursor.id})` ); } @@ -187,6 +186,7 @@ export async function findUserListBatch( providerGroup: users.providerGroup, tags: users.tags, createdAt: users.createdAt, + createdAtRaw: sql`to_char(${users.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`, updatedAt: users.updatedAt, deletedAt: users.deletedAt, limit5hUsd: users.limit5hUsd, @@ -211,8 +211,8 @@ export async function findUserListBatch( const lastUser = usersToReturn[usersToReturn.length - 1]; const nextCursor = - hasMore && lastUser?.createdAt - ? { createdAt: lastUser.createdAt.toISOString(), id: lastUser.id } + hasMore && lastUser?.createdAtRaw + ? { createdAt: lastUser.createdAtRaw, id: lastUser.id } : null; return {