diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-outcome-distribution-section.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-outcome-distribution-section.tsx index cdfbe60ee2..85e80da54f 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-outcome-distribution-section.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-outcome-distribution-section.tsx @@ -1,4 +1,5 @@ import { Card, CardContent } from "@/components/ui/card"; +import { formatNumber } from "@/lib/fmt"; import { cn } from "@/lib/utils"; import { Clone } from "@unkey/icons"; import { Button } from "@unkey/ui"; @@ -54,7 +55,7 @@ export const OutcomeDistributionSection = ({ {formatOutcomeName(outcome)}: - {count.toLocaleString()} + {formatNumber(count)} ))} diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-section.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-section.tsx index 281aae4289..cc21f4ec30 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-section.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/log-section.tsx @@ -1,40 +1,19 @@ "use client"; import { Card, CardContent } from "@/components/ui/card"; import { toast } from "@/components/ui/toaster"; -import { Button } from "@unkey/ui"; - -import { TimestampInfo } from "@/components/timestamp-info"; import { Clone } from "@unkey/icons"; -import { isValid, parse, parseISO } from "date-fns"; - -const TIME_KEYWORDS = [ - "created", - "created_at", - "createdAt", - "updated", - "updated_at", - "updatedAt", - "time", - "date", - "timestamp", - "expires", - "expired", - "expiration", - "last", - "refill_at", - "used", -]; +import { Button } from "@unkey/ui"; export const LogSection = ({ details, title, }: { - details: string | string[]; + details: Record | string; title: string; }) => { const handleClick = () => { navigator.clipboard - .writeText(getFormattedContent(details)) + .writeText(JSON.stringify(details)) .then(() => { toast.success(`${title} copied to clipboard`); }) @@ -52,29 +31,16 @@ export const LogSection = ({
-            {Array.isArray(details)
-              ? details.map((header) => {
-                  const [key, ...valueParts] = header.split(":");
-                  const value = valueParts.join(":").trim();
-
-                  // Check if this is a timestamp field we should enhance
-                  const keyLower = key.toLowerCase();
-                  const isTimeField = TIME_KEYWORDS.some((keyword) => keyLower.includes(keyword));
-                  const shouldEnhance = isTimeField && isTimeValue(value);
-
+            {typeof details === "object"
+              ? Object.entries(details).map((detail) => {
+                  const [key, value] = detail;
                   return (
                     
{key} {value ? ":" : ""} - {shouldEnhance ? ( - - - - ) : ( - {value} - )} + {value}
); }) @@ -94,55 +60,3 @@ export const LogSection = ({ ); }; - -const getFormattedContent = (details: string | string[]) => { - if (Array.isArray(details)) { - return details - .map((header) => { - const [key, ...valueParts] = header.split(":"); - const value = valueParts.join(":").trim(); - return `${key}: ${value}`; - }) - .join("\n"); - } - return details; -}; - -const isTimeValue = (value: string): boolean => { - // Skip non-timestamp values - if ( - value === "N/A" || - value === "Invalid Date" || - value.startsWith("Less than") || - /^\d+ (day|hour|minute|second)s?$/.test(value) - ) { - return false; - } - - try { - // Handle ISO format strings - if (/^\d{4}-\d{2}-\d{2}/.test(value)) { - return isValid(parseISO(value)); - } - - // Handle common localized formats - if (/\d{1,2}\/\d{1,2}\/\d{4}/.test(value)) { - // Try US format first: MM/DD/YYYY - const datePart = value.split(",")[0]; - const parsedDate = parse(datePart, "M/d/yyyy", new Date()); - return isValid(parsedDate); - } - - // Handle month name formats - if (/[A-Za-z]{3}\s\d{1,2},\s\d{4}/.test(value)) { - const parsedDate = parse(value, "MMM d, yyyy", new Date()); - return isValid(parsedDate); - } - - // Fallback to standard JS date parsing - const date = new Date(value); - return !Number.isNaN(date.getTime()); - } catch { - return false; - } -}; diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/roles-permissions.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/roles-permissions.tsx index 86767718f0..f81baef930 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/roles-permissions.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/components/roles-permissions.tsx @@ -85,7 +85,7 @@ type PermissionsSectionProps = { permissions: Permission[]; }; -export const PermissionsSection: React.FC = ({ permissions }) => { +export const PermissionsSection = ({ permissions }: PermissionsSectionProps) => { const handleCopy = (permission: Permission) => { const content = `${permission.name}${ permission.description ? `\n${permission.description}` : "" diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/index.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/index.tsx index 5856352a7d..78c729fed8 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/index.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/log-details/index.tsx @@ -1,7 +1,9 @@ "use client"; import { DEFAULT_DRAGGABLE_WIDTH } from "@/app/(app)/logs/constants"; import { ResizablePanel } from "@/components/logs/details/resizable-panel"; +import { TimestampInfo } from "@/components/timestamp-info"; import type { KeysOverviewLog } from "@unkey/clickhouse/src/keys/keys"; +import Link from "next/link"; import { useMemo } from "react"; import { LogHeader } from "./components/log-header"; import { OutcomeDistributionSection } from "./components/log-outcome-distribution-section"; @@ -25,6 +27,7 @@ const createPanelStyle = (distanceToTop: number): StyleObject => ({ type KeysOverviewLogDetailsProps = { distanceToTop: number; log: KeysOverviewLog | null; + apiId: string; setSelectedLog: (data: KeysOverviewLog | null) => void; }; @@ -32,6 +35,7 @@ export const KeysOverviewLogDetails = ({ distanceToTop, log, setSelectedLog, + apiId, }: KeysOverviewLogDetailsProps) => { const panelStyle = useMemo(() => createPanelStyle(distanceToTop), [distanceToTop]); @@ -59,32 +63,45 @@ export const KeysOverviewLogDetails = ({ // Process key details data const metaData = formatMeta(log.key_details.meta); - const createdAt = metaData?.createdAt - ? formatDate(metaData.createdAt.replace(/3NZ$/, "3Z")) - : "N/A"; - - const identifiers = [`ID: ${log.key_details.id}`, `Name: ${log.key_details.name || "N/A"}`]; + const identifiers = { + "Key ID": ( + +
{log.key_id}
+ + ), + Name: log.key_details.name || "N/A", + }; - const usage = [`Created: ${createdAt}`, `Last Used: ${log.time ? formatDate(log.time) : "N/A"}`]; + const usage = { + Created: metaData?.createdAt ? metaData.createdAt : "N/A", + "Last Used": log.time ? ( + + ) : ( + "N/A" + ), + }; - const limits = [ - `Status: ${log.key_details.enabled ? "Enabled" : "Disabled"}`, - `Remaining: ${ - log.key_details.remaining_requests !== null ? log.key_details.remaining_requests : "Unlimited" - }`, - `Rate Limit: ${ - log.key_details.ratelimit_limit - ? `${log.key_details.ratelimit_limit} per ${log.key_details.ratelimit_duration || "N/A"}s` - : "No limit" - }`, - `Async: ${log.key_details.ratelimit_async ? "Yes" : "No"}`, - ]; + const limits = { + Status: log.key_details.enabled ? "Enabled" : "Disabled", + Remaining: + log.key_details.remaining_requests !== null + ? log.key_details.remaining_requests + : "Unlimited", + "Rate Limit": log.key_details.ratelimit_limit + ? `${log.key_details.ratelimit_limit} per ${log.key_details.ratelimit_duration || "N/A"}s` + : "No limit", + Async: log.key_details.ratelimit_async ? "Yes" : "No", + }; const identity = log.key_details.identity - ? [`External ID: ${log.key_details.identity.external_id || "N/A"}`] - : ["No identity connected"]; + ? { "External ID": log.key_details.identity.external_id || "N/A" } + : { "No identity connected": null }; - const metaString = metaData ? JSON.stringify(metaData, null, 2) : ""; + const metaString = metaData ? JSON.stringify(metaData, null, 2) : { "No meta available": "" }; return ( - ); }; -const formatDate = (date: string | number | Date | null): string => { - if (!date) { - return "N/A"; - } - try { - if (date instanceof Date) { - return date.toLocaleString(); - } - return new Date(date).toLocaleString(); - } catch { - return "Invalid Date"; - } -}; - const formatMeta = (meta: string | null): Record | null => { if (!meta) { return null; diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx index 9a39d3b130..ff95f96a86 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"; import type { KeysOverviewLog } from "@unkey/clickhouse/src/keys/keys"; import { TriangleWarning2 } from "@unkey/icons"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@unkey/ui"; +import Link from "next/link"; import { getErrorPercentage, getErrorSeverity } from "../utils/calculate-blocked-percentage"; export const KeyTooltip = ({ @@ -29,6 +30,7 @@ export const KeyTooltip = ({ type KeyIdentifierColumnProps = { log: KeysOverviewLog; + apiId: string; }; // Get warning icon based on error severity @@ -59,7 +61,7 @@ const getWarningMessage = (severity: string, errorRate: number) => { } }; -export const KeyIdentifierColumn = ({ log }: KeyIdentifierColumnProps) => { +export const KeyIdentifierColumn = ({ log, apiId }: KeyIdentifierColumnProps) => { const errorPercentage = getErrorPercentage(log); const severity = getErrorSeverity(log); const hasErrors = severity !== "none"; @@ -73,10 +75,16 @@ export const KeyIdentifierColumn = ({ log }: KeyIdentifierColumnProps) => { {getWarningIcon(severity)} -
- {log.key_id.substring(0, 8)}... - {log.key_id.substring(log.key_id.length - 4)} -
+ +
+ {log.key_id.substring(0, 8)}... + {log.key_id.substring(log.key_id.length - 4)} +
+ ); }; diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx index 2ced272439..e7c5b0a59f 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx @@ -36,7 +36,7 @@ export const KeysOverviewLogsTable = ({ apiId, setSelectedLog, log: selectedLog header: "ID", width: "15%", headerClassName: "pl-11", - render: (log) => , + render: (log) => , }, { key: "name", @@ -154,7 +154,7 @@ export const KeysOverviewLogsTable = ({ apiId, setSelectedLog, log: selectedLog direction: getSortDirection("time"), sortable: true, onSort() { - toggleSort("time", false); + toggleSort("time", false, "asc"); }, }, render: (log) => ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/logs-client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/logs-client.tsx index ad75503e23..6d72e5fb62 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/logs-client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/logs-client.tsx @@ -27,6 +27,7 @@ export const LogsClient = ({ apiId }: { apiId: string }) => { ( diff --git a/apps/dashboard/components/logs/hooks/use-sort.tsx b/apps/dashboard/components/logs/hooks/use-sort.tsx index c01bd710a5..66c29e480a 100644 --- a/apps/dashboard/components/logs/hooks/use-sort.tsx +++ b/apps/dashboard/components/logs/hooks/use-sort.tsx @@ -27,7 +27,7 @@ export function useSort(paramName = "sorts") { ); const toggleSort = useCallback( - (columnKey: TSortFields, multiSort = false) => { + (columnKey: TSortFields, multiSort = false, order: "asc" | "desc" = "desc") => { const currentSort = sortParams?.find((sort) => sort.column === columnKey); const otherSorts = sortParams?.filter((sort) => sort.column !== columnKey) ?? []; let newSorts: SortUrlValue[]; @@ -35,8 +35,8 @@ export function useSort(paramName = "sorts") { if (!currentSort) { // Add new sort newSorts = multiSort - ? [...(sortParams ?? []), { column: columnKey, direction: "asc" }] - : [{ column: columnKey, direction: "asc" }]; + ? [...(sortParams ?? []), { column: columnKey, direction: order }] + : [{ column: columnKey, direction: order }]; } else if (currentSort.direction === "asc") { // Toggle to desc newSorts = multiSort diff --git a/apps/dashboard/lib/trpc/routers/utils/granularity.ts b/apps/dashboard/lib/trpc/routers/utils/granularity.ts index f58b86b60f..3b91d096b9 100644 --- a/apps/dashboard/lib/trpc/routers/utils/granularity.ts +++ b/apps/dashboard/lib/trpc/routers/utils/granularity.ts @@ -89,7 +89,7 @@ export const getTimeseriesGranularity = ( } else if (timeRange >= MONTH_IN_MS) { granularity = "per3Days"; } else if (timeRange >= WEEK_IN_MS * 2) { - granularity = "perHour"; + granularity = "per6Hours"; } else if (timeRange >= WEEK_IN_MS) { granularity = "perHour"; } else {