Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -54,7 +55,7 @@ export const OutcomeDistributionSection = ({
<span>{formatOutcomeName(outcome)}:</span>
</div>
<span className="ml-2 text-xs text-accent-12 truncate font-mono tabular-nums">
{count.toLocaleString()}
{formatNumber(count)}
</span>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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, React.ReactNode> | string;
title: string;
}) => {
const handleClick = () => {
navigator.clipboard
.writeText(getFormattedContent(details))
.writeText(JSON.stringify(details))
.then(() => {
toast.success(`${title} copied to clipboard`);
})
Expand All @@ -52,29 +31,16 @@ export const LogSection = ({
<Card className="bg-gray-2 border-gray-4 rounded-lg">
<CardContent className="py-2 px-3 text-xs relative group">
<pre className="flex flex-col gap-1 whitespace-pre-wrap leading-relaxed">
{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 (
<div className="group flex items-center w-full p-[3px]" key={key}>
<span className="text-left text-accent-9 whitespace-nowrap">
{key}
{value ? ":" : ""}
</span>
{shouldEnhance ? (
<span className="ml-2 text-xs text-accent-12 truncate">
<TimestampInfo value={value} />
</span>
) : (
<span className="ml-2 text-xs text-accent-12 truncate">{value}</span>
)}
<span className="ml-2 text-xs text-accent-12 truncate">{value}</span>
</div>
);
})
Expand All @@ -94,55 +60,3 @@ export const LogSection = ({
</div>
);
};

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;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type PermissionsSectionProps = {
permissions: Permission[];
};

export const PermissionsSection: React.FC<PermissionsSectionProps> = ({ permissions }) => {
export const PermissionsSection = ({ permissions }: PermissionsSectionProps) => {
const handleCopy = (permission: Permission) => {
const content = `${permission.name}${
permission.description ? `\n${permission.description}` : ""
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,13 +27,15 @@ const createPanelStyle = (distanceToTop: number): StyleObject => ({
type KeysOverviewLogDetailsProps = {
distanceToTop: number;
log: KeysOverviewLog | null;
apiId: string;
setSelectedLog: (data: KeysOverviewLog | null) => void;
};

export const KeysOverviewLogDetails = ({
distanceToTop,
log,
setSelectedLog,
apiId,
}: KeysOverviewLogDetailsProps) => {
const panelStyle = useMemo(() => createPanelStyle(distanceToTop), [distanceToTop]);

Expand Down Expand Up @@ -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": (
<Link
title={`View details for ${log.key_id}`}
className="font-mono underline decoration-dotted"
href={`/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
>
<div className="font-mono font-medium truncate">{log.key_id}</div>
</Link>
),
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 ? (
<TimestampInfo value={log.time} className="font-mono underline decoration-dotted" />
) : (
"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) : "<EMPTY>";
const metaString = metaData ? JSON.stringify(metaData, null, 2) : { "No meta available": "" };

return (
<ResizablePanel
Expand All @@ -100,26 +117,11 @@ export const KeysOverviewLogDetails = ({
<LogSection title="Identity" details={identity} />
<RolesSection roles={log.key_details.roles || []} />
<PermissionsSection permissions={log.key_details.permissions || []} />

<LogSection title="Meta" details={metaString} />
</ResizablePanel>
);
};

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<string, any> | null => {
if (!meta) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand All @@ -29,6 +30,7 @@ export const KeyTooltip = ({

type KeyIdentifierColumnProps = {
log: KeysOverviewLog;
apiId: string;
};

// Get warning icon based on error severity
Expand Down Expand Up @@ -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";
Expand All @@ -73,10 +75,16 @@ export const KeyIdentifierColumn = ({ log }: KeyIdentifierColumnProps) => {
{getWarningIcon(severity)}
</div>
</KeyTooltip>
<div className="font-mono font-medium truncate">
{log.key_id.substring(0, 8)}...
{log.key_id.substring(log.key_id.length - 4)}
</div>
<Link
title={`View details for ${log.key_id}`}
className="font-mono group-hover:underline decoration-dotted"
href={`/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
>
<div className="font-mono font-medium truncate">
{log.key_id.substring(0, 8)}...
{log.key_id.substring(log.key_id.length - 4)}
</div>
</Link>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const KeysOverviewLogsTable = ({ apiId, setSelectedLog, log: selectedLog
header: "ID",
width: "15%",
headerClassName: "pl-11",
render: (log) => <KeyIdentifierColumn log={log} />,
render: (log) => <KeyIdentifierColumn log={log} apiId={apiId} />,
},
{
key: "name",
Expand Down Expand Up @@ -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) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const LogsClient = ({ apiId }: { apiId: string }) => {
<KeysOverviewLogsCharts apiId={apiId} onMount={handleDistanceToTop} />
<KeysOverviewLogsTable apiId={apiId} setSelectedLog={handleSelectedLog} log={selectedLog} />
<KeysOverviewLogDetails
apiId={apiId}
distanceToTop={tableDistanceToTop}
setSelectedLog={handleSelectedLog}
log={selectedLog}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const RatelimitOverviewLogsTable = ({
direction: getSortDirection("time"),
sortable: true,
onSort() {
toggleSort("time", false);
toggleSort("time", false, "asc");
},
},
render: (log) => (
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/components/logs/hooks/use-sort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ export function useSort<TSortFields extends string>(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<TSortFields>[];

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
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/lib/trpc/routers/utils/granularity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const getTimeseriesGranularity = <TContext extends TimeseriesContext>(
} 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 {
Expand Down
Loading