Skip to content
Open
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,19 +1,25 @@
"use client";
import { VirtualTable } from "@/components/virtual-table/index";
import type { Column } from "@/components/virtual-table/types";
import { cn } from "@/lib/utils";
import { createApiRequestColumns } from "@/components/api-requests-table/columns/create-api-request-columns";
import { useKeysOverviewLogsQuery } from "@/components/api-requests-table/hooks/use-keys-overview-query";
import { sortFields } from "@/components/api-requests-table/schema/keys-overview.schema";
import type { SortFields } from "@/components/api-requests-table/schema/keys-overview.schema";
import { getRowClassName } from "@/components/api-requests-table/utils/get-row-class";
import { useSort } from "@/components/logs/hooks/use-sort";
import type { RowSelectionState, SortingState } from "@tanstack/react-table";
import type { KeysOverviewLog } from "@unkey/clickhouse/src/keys/keys";
import { Ban, BookBookmark } from "@unkey/icons";
import { Badge, Button, Empty, TimestampInfo } from "@unkey/ui";
import { DataTable, type DataTableConfig, EmptyApiRequests } from "@unkey/ui";
import { useCallback, useMemo } from "react";

import { useSort } from "@/components/logs/hooks/use-sort";
import { formatNumber } from "@/lib/fmt";
import { OutcomesPopover } from "./components/outcome-popover";
import { KeyIdentifierColumn } from "./components/override-indicator";
import { useKeysOverviewLogsQuery } from "./hooks/use-logs-query";
import type { SortFields } from "./query-logs.schema";
import { getErrorPercentage, getSuccessPercentage } from "./utils/calculate-blocked-percentage";
import { STATUS_STYLES, getRowClassName, getStatusStyle } from "./utils/get-row-class";
const TABLE_CONFIG: DataTableConfig = {
rowHeight: 26, // compact rows, default is 36
rowSpacing: 4,
headerHeight: 40,
layout: "classic" as const,
rowBorders: false,
containerPadding: "px-2",
tableLayout: "fixed",
loadingRows: 10,
};

type Props = {
log: KeysOverviewLog | null;
Expand All @@ -22,192 +28,69 @@ type Props = {
};

export const KeysOverviewLogsTable = ({ apiId, setSelectedLog, log: selectedLog }: Props) => {
const { getSortDirection, toggleSort } = useSort<SortFields>();
const { historicalLogs, isLoading, isLoadingMore, loadMore } = useKeysOverviewLogsQuery({
apiId,
});
const { sorts, setSorts } = useSort<SortFields>();
const { historicalLogs, isLoading, hasMore } = useKeysOverviewLogsQuery({ apiId });

const handleNavigate = useCallback(() => setSelectedLog(null), [setSelectedLog]);

const columns = useMemo(
() => createApiRequestColumns({ apiId, onNavigate: handleNavigate }),
[apiId, handleNavigate],
);

const columns = (): Column<KeysOverviewLog>[] => {
return [
{
key: "key_id",
header: "ID",
width: "15%",
headerClassName: "pl-12",
render: (log) => (
<KeyIdentifierColumn log={log} apiId={apiId} onNavigate={() => setSelectedLog(null)} />
),
},
{
key: "name",
header: "Name",
width: "15%",
render: (log) => {
const name = log.key_details?.name || "—";
return (
<div className="flex items-center font-mono">
<div className="w-full max-w-[150px] truncate whitespace-nowrap" title={name}>
{name}
</div>
</div>
);
},
},
{
key: "external_id",
header: "External ID",
width: "15%",
render: (log) => {
const externalId =
(log.key_details?.identity?.external_id ?? log.key_details?.owner_id) || "—";
return (
<div className="flex items-center font-mono">
<div className="w-full max-w-[150px] truncate whitespace-nowrap" title={externalId}>
{externalId}
</div>
</div>
);
},
},
{
key: "valid",
header: "Valid",
width: "15%",
sort: {
direction: getSortDirection("valid"),
sortable: true,
onSort() {
toggleSort("valid", false);
},
},
render: (log) => {
const successPercentage = getSuccessPercentage(log);
return (
<div className="flex gap-3 items-center tabular-nums">
<Badge
className={cn(
"px-[6px] rounded-md font-mono whitespace-nowrap",
selectedLog?.key_id === log.key_id
? STATUS_STYLES.success.badge.selected
: STATUS_STYLES.success.badge.default,
)}
title={`${log.valid_count.toLocaleString()} Valid requests (${successPercentage.toFixed(
1,
)}%)`}
>
{formatNumber(log.valid_count)}
</Badge>
</div>
);
},
},
{
key: "invalid",
header: "Invalid",
width: "15%",
sort: {
direction: getSortDirection("invalid"),
sortable: true,
onSort() {
toggleSort("invalid", false);
},
},
render: (log) => {
const style = getStatusStyle(log);
const errorPercentage = getErrorPercentage(log);
const rowSelection = useMemo<RowSelectionState>(
() => (selectedLog ? { [selectedLog.request_id]: true } : {}),
[selectedLog],
);

const sorting: SortingState = useMemo(
() =>
sorts.length > 0
? sorts.map((s) => ({ id: s.column, desc: s.direction === "desc" }))
: [{ id: "time", desc: true }],
[sorts],
);

return (
<div className="flex items-center w-full">
<div className="shrink-0">
<Badge
className={cn(
"px-[6px] rounded-md font-mono whitespace-nowrap flex items-center",
selectedLog?.key_id === log.key_id ? style.badge.selected : style.badge.default,
)}
title={`${log.error_count.toLocaleString()} Invalid requests (${errorPercentage.toFixed(
1,
)}%)`}
>
<span className="mr-[6px] shrink-0">
<Ban iconSize="sm-regular" />
</span>
<span className="overflow-hidden text-ellipsis whitespace-nowrap max-w-[45px]">
{formatNumber(log.error_count)}
</span>
</Badge>
</div>
<div className="ml-2 shrink-0">
<OutcomesPopover
outcomeCounts={log.outcome_counts}
isSelected={selectedLog?.key_id === log.key_id}
/>
</div>
</div>
);
},
},
{
key: "lastUsed",
header: "Last Used",
width: "15%",
sort: {
direction: getSortDirection("time"),
sortable: true,
onSort() {
toggleSort("time", false, "asc");
},
},
render: (log) => (
<TimestampInfo
value={log.time}
className={cn(
"font-mono group-hover:underline decoration-dotted",
selectedLog && selectedLog.request_id !== log.request_id && "pointer-events-none",
)}
/>
),
},
];
};
const handleSortingChange = useCallback(
(updater: SortingState | ((old: SortingState) => SortingState)) => {
const next = typeof updater === "function" ? updater(sorting) : updater;
const validated = next.flatMap((s) => {
const result = sortFields.safeParse(s.id);
if (!result.success) {
return [];
}
return [{ column: result.data, direction: s.desc ? ("desc" as const) : ("asc" as const) }];
});
setSorts(validated);
},
[sorting, setSorts],
);

const getRowClassNameMemoized = useCallback(
(log: KeysOverviewLog) => getRowClassName(log, selectedLog),
[selectedLog],
);

return (
<VirtualTable
<DataTable
data={historicalLogs}
isLoading={isLoading}
isFetchingNextPage={isLoadingMore}
onLoadMore={loadMore}
columns={columns()}
columns={columns}
getRowId={(log) => log.request_id}
onRowClick={setSelectedLog}
selectedItem={selectedLog}
keyExtractor={(log) => log.request_id}
rowClassName={(log) => getRowClassName(log, selectedLog as KeysOverviewLog)}
rowClassName={getRowClassNameMemoized}
sorting={sorting}
onSortingChange={handleSortingChange}
manualSorting={true}
enableRowSelection={true}
rowSelection={rowSelection}
config={TABLE_CONFIG}
loadMoreFooterProps={{
hide: true,
hasMore: hasMore ?? false,
}}
emptyState={
<div className="w-full flex justify-center items-center h-full">
<Empty className="w-[400px] flex items-start">
<Empty.Icon className="w-auto" />
<Empty.Title>Key Verification Logs</Empty.Title>
<Empty.Description className="text-left">
No key verification data to show. Once requests are made with API keys, you'll see a
summary of successful and failed verification attempts.
</Empty.Description>{" "}
<Empty.Actions className="mt-4 justify-center md:justify-start">
<a
href="https://www.unkey.com/docs/introduction"
target="_blank"
rel="noopener noreferrer"
>
<Button size="md">
<BookBookmark />
Documentation
</Button>
</a>
</Empty.Actions>
</Empty>
</div>
}
emptyState={<EmptyApiRequests />}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";

import { KeysOverviewLogDetails } from "@/components/api-requests-table/components/log-details";
import type { KeysOverviewLog } from "@unkey/clickhouse/src/keys/keys";
import { useCallback, useState } from "react";
import { KeysOverviewLogsCharts } from "./components/charts";
import { KeysOverviewLogsControlCloud } from "./components/control-cloud";
import { KeysOverviewLogsControls } from "./components/controls";
import { KeysOverviewLogDetails } from "./components/table/components/log-details";
import { KeysOverviewLogsTable } from "./components/table/logs-table";

export const LogsClient = ({ apiId }: { apiId: string }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,23 +94,6 @@ export const getStatusStyle = (log: KeysOverviewLog): StatusStyle => {
}
};

export const getOutcomeBadgeClass = (outcome: string): string => {
const severity = categorizeSeverity(outcome);

switch (severity) {
case "error":
return "bg-error-4 text-error-11";
case "moderate":
return "bg-orange-4 text-orange-11";
case "warning":
return "bg-warning-4 text-warning-11";
case "success":
return "bg-accent-4 text-accent-11";
default:
return "bg-gray-4 text-gray-11";
}
};

export const getRowClassName = (log: KeysOverviewLog, selectedLog: KeysOverviewLog) => {
const style = getStatusStyle(log);
const isSelected = log.key_id === selectedLog?.key_id;
Expand Down
Loading