Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aff2e34
feat: add new layout for logs page
ogzhanolguncu Jan 2, 2025
2b99fdf
feat: add charts
ogzhanolguncu Jan 2, 2025
3232d4e
fix: import paths
ogzhanolguncu Jan 3, 2025
f19c261
fix: colors
ogzhanolguncu Jan 3, 2025
0e69036
refactor: colors of timestamp info
ogzhanolguncu Jan 3, 2025
0280478
feat: finish table rows
ogzhanolguncu Jan 3, 2025
06cd6d7
Merge branch 'main' of github.com:unkeyed/unkey into logs-v2
ogzhanolguncu Jan 3, 2025
f3ec2b1
feat: fix log details to chart
ogzhanolguncu Jan 6, 2025
c62ef04
refactor: clean up log details
ogzhanolguncu Jan 6, 2025
fc0429f
feat: add new log details header section
ogzhanolguncu Jan 6, 2025
79c8a81
refactor: improve feel of virtual table
ogzhanolguncu Jan 6, 2025
7709226
feat: finalize log details
ogzhanolguncu Jan 6, 2025
efeba35
refactor: redundant null check
ogzhanolguncu Jan 6, 2025
574e262
refactor: adjust texts
ogzhanolguncu Jan 7, 2025
d0ff121
refactor: remove old virtual table and make it more readable
ogzhanolguncu Jan 7, 2025
4817c63
feat: add new chart tooltip
ogzhanolguncu Jan 7, 2025
624c27d
chore: fix build step
ogzhanolguncu Jan 7, 2025
2facb80
chore: run formatter
ogzhanolguncu Jan 8, 2025
f1694e7
chore: run formatter
ogzhanolguncu Jan 8, 2025
965186a
feat: update icons
ogzhanolguncu Jan 8, 2025
125c084
fix: style issue due to virtual table changes
ogzhanolguncu Jan 8, 2025
8c8bb8e
Merge branch 'main' of github.com:unkeyed/unkey into logs-v2
ogzhanolguncu Jan 8, 2025
6291f6e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 8, 2025
ed56a43
feat: add feature flog for logs-v2
ogzhanolguncu Jan 8, 2025
6393bad
Merge branch 'logs-v2' of github.com:unkeyed/unkey into logs-v2
ogzhanolguncu Jan 8, 2025
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
Expand Up @@ -107,7 +107,9 @@ export const AuditLogTableClient = () => {
renderDetails={(log, onClose, distanceToTop) => (
<LogDetails log={log} onClose={onClose} distanceToTop={distanceToTop} />
)}
loadingRows={DEFAULT_FETCH_COUNT}
config={{
loadingRows: DEFAULT_FETCH_COUNT,
}}
/>
);
};
33 changes: 22 additions & 11 deletions apps/dashboard/app/(app)/audit/components/table/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TimestampInfo } from "@/components/timestamp-info";
import { Badge } from "@/components/ui/badge";
import type { Column } from "@/components/virtual-table";
import type { Column } from "@/components/virtual-table/types";
import { cn } from "@unkey/ui/src/lib/utils";
import { FunctionSquare, KeySquare } from "lucide-react";
import type { Data } from "./types";
Expand All @@ -10,20 +10,24 @@ export const columns: Column<Data>[] = [
{
key: "time",
header: "Time",
width: "130px",
headerClassName: "pl-3",
width: "150px",
render: (log) => (
<TimestampInfo
value={log.auditLog.time}
className="font-mono group-hover:underline decoration-dotted"
/>
<div className="flex items-center gap-3 px-2">
<TimestampInfo
value={log.auditLog.time}
className="font-mono group-hover:underline decoration-dotted"
/>
</div>
),
},
{
key: "actor",
header: "Actor",
width: "10%",
headerClassName: "pl-3",
width: "7%",
render: (log) => (
<div className="flex items-center">
<div className="flex items-center gap-3 px-2">
{log.auditLog.actor.type === "user" && log.user ? (
<div className="flex items-center w-full gap-2 max-sm:m-0 max-sm:gap-1 max-sm:text-xs">
<span className="text-xs whitespace-nowrap">{`${log.user.firstName ?? ""} ${
Expand All @@ -47,7 +51,8 @@ export const columns: Column<Data>[] = [
{
key: "action",
header: "Action",
width: "72px",
headerClassName: "pl-3",
width: "7%",
render: (log) => {
const eventType = getEventType(log.auditLog.event);
const badgeClassName = cn("font-mono capitalize", {
Expand All @@ -56,22 +61,28 @@ export const columns: Column<Data>[] = [
"bg-success-3 text-success-11 hover:bg-success-4": eventType === "create",
"bg-accent-3 text-accent-11 hover:bg-accent-4": eventType === "other",
});
return <Badge className={badgeClassName}>{eventType}</Badge>;
return (
<div className="flex items-center gap-3 px-2">
<Badge className={badgeClassName}>{eventType}</Badge>
</div>
);
},
},
{
key: "event",
header: "Event",
headerClassName: "pl-2",
width: "20%",
render: (log) => (
<div className="flex items-center gap-2 text-current font-mono text-xs">
<div className="flex items-center gap-2 text-current font-mono text-xs px-2">
<span>{log.auditLog.event}</span>
</div>
),
},
{
key: "event-description",
header: "Description",
headerClassName: "pl-1",
width: "auto",
render: (log) => (
<div className="text-current font-mono px-2 text-xs">{log.auditLog.description}</div>
Expand Down
155 changes: 155 additions & 0 deletions apps/dashboard/app/(app)/logs-v2/components/charts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use client";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Grid } from "@unkey/icons";
import { addMinutes, format } from "date-fns";
import { useEffect, useRef } from "react";
import { Bar, BarChart, ResponsiveContainer, YAxis } from "recharts";
import { generateMockLogsData } from "./util";

const chartConfig = {
success: {
label: "Success",
subLabel: "2xx",
color: "hsl(var(--accent-4))",
},
warning: {
label: "Warning",
subLabel: "4xx",
color: "hsl(var(--warning-9))",
},
error: {
label: "Error",
subLabel: "5xx",
color: "hsl(var(--error-9))",
},
} satisfies ChartConfig;

const formatTimestampTooltip = (value: string | number) => {
const date = new Date(value);
const offset = new Date().getTimezoneOffset() * -1;
const localDate = addMinutes(date, offset);
return format(localDate, "MMM dd HH:mm:ss.SS aa");
};

const formatTimestampLabel = (timestamp: string | number | Date) => {
const date = new Date(timestamp);
return format(date, "MMM dd, h:mma").toUpperCase();
};

type Timeseries = {
x: number;
displayX: string;
originalTimestamp: string;
success: number;
error: number;
warning: number;
total: number;
};

const calculateTimePoints = (timeseries: Timeseries[]) => {
const startTime = timeseries[0].x;
const endTime = timeseries.at(-1)?.x;
const timeRange = endTime ?? 0 - startTime;
const timePoints = Array.from({ length: 5 }, (_, i) => {
return new Date(startTime + (timeRange * i) / 5);
});
return timePoints;
};

const timeseries = generateMockLogsData(24, 10);

export function LogsChart({
onMount,
}: {
onMount: (distanceToTop: number) => void;
}) {
const chartRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const distanceToTop = chartRef.current?.getBoundingClientRect().top ?? 0;
onMount(distanceToTop);
}, [onMount]);

return (
<div className="w-full relative" ref={chartRef}>
<div className="px-2 text-accent-11 font-mono absolute top-0 text-xxs w-full flex justify-between">
{calculateTimePoints(timeseries).map((time, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: use of index is acceptable here.
<div key={i}>{formatTimestampLabel(time)}</div>
))}
</div>
<ResponsiveContainer width="100%" height={50} className="border-b border-gray-4">
<ChartContainer config={chartConfig}>
<BarChart data={timeseries} barGap={2} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
<YAxis domain={[0, (dataMax: number) => dataMax * 1.5]} hide />
<ChartTooltip
position={{ y: 50 }}
isAnimationActive
wrapperStyle={{ zIndex: 1000 }}
cursor={{
fill: "hsl(var(--accent-3))",
strokeWidth: 1,
strokeDasharray: "5 5",
strokeOpacity: 0.7,
}}
content={({ active, payload, label }) => {
if (!active || !payload?.length) {
return null;
}

return (
<ChartTooltipContent
payload={payload}
label={label}
active={active}
bottomExplainer={
<div className="grid gap-1.5 pt-2 border-t border-gray-4">
<div className="flex w-full [&>svg]:size-4 gap-4 px-4 items-center">
<Grid className="text-gray-6" />
<div className="flex gap-4 leading-none justify-between w-full py-1 items-center">
<div className="flex gap-4 items-center min-w-[80px]">
<span className="capitalize text-accent-9 text-xs w-[2ch] inline-block">
All
</span>
<span className="capitalize text-accent-12 text-xs">Total</span>
</div>
<div className="ml-auto">
<span className="font-mono tabular-nums text-accent-12">
{payload[0]?.payload?.total}
</span>
</div>
</div>
</div>
</div>
}
className="rounded-lg shadow-lg border border-gray-4"
labelFormatter={(_, tooltipPayload) => {
const originalTimestamp = tooltipPayload[0]?.payload?.originalTimestamp;
return originalTimestamp ? (
<div>
<span className="font-mono text-accent-9 text-xs px-4">
{formatTimestampTooltip(originalTimestamp)}
</span>
</div>
) : (
""
);
}}
/>
);
}}
/>
{["success", "error", "warning"].map((key) => (
<Bar key={key} dataKey={key} stackId="a" fill={`var(--color-${key})`} />
))}
</BarChart>
</ChartContainer>
</ResponsiveContainer>
</div>
);
}
31 changes: 31 additions & 0 deletions apps/dashboard/app/(app)/logs-v2/components/charts/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Generates mock time series data for logs visualization
* @param hours Number of hours of data to generate
* @param intervalMinutes Interval between data points in minutes
* @returns Array of LogsTimeseriesDataPoint
*/
export function generateMockLogsData(hours = 24, intervalMinutes = 5) {
const now = new Date();
const points = Math.floor((hours * 60) / intervalMinutes);
const data = [];

for (let i = points - 1; i >= 0; i--) {
const timestamp = new Date(now.getTime() - i * intervalMinutes * 60 * 1000);

const success = Math.floor(Math.random() * 50) + 20;
const error = Math.floor(Math.random() * 30);
const warning = Math.floor(Math.random() * 25);

data.push({
x: Math.floor(timestamp.getTime()),
displayX: timestamp.toISOString(),
originalTimestamp: timestamp.toISOString(),
success,
error,
warning,
total: success + error + warning,
});
}

return data;
}
46 changes: 46 additions & 0 deletions apps/dashboard/app/(app)/logs-v2/components/filters/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
BarsFilter,
Calendar,
CircleCarretRight,
Magnifier,
Refresh3,
Sliders,
} from "@unkey/icons";

export function LogsFilters() {
return (
<div className="flex flex-col border-b border-gray-4 ">
<div className="px-3 py-2 w-full justify-between flex items-center min-h-10">
<div className="flex gap-2">
<div className="flex gap-2 items-center px-2">
<Magnifier className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Search logs...</span>
</div>
<div className="flex gap-2 items-center px-2">
<BarsFilter className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Filter</span>
</div>
<div className="flex gap-2 items-center px-2">
<Calendar className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Last 24 hours</span>
</div>
</div>

<div className="flex gap-2">
<div className="flex gap-2 items-center px-2">
<CircleCarretRight className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Live</span>
</div>
<div className="flex gap-2 items-center px-2">
<Refresh3 className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Refresh</span>
</div>
<div className="flex gap-2 items-center px-2">
<Sliders className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Display</span>
</div>
</div>
</div>
</div>
);
}
Comment on lines +10 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add interactivity and accessibility to filter controls

The filter controls appear to be static UI elements without any interaction handlers. Consider the following improvements:

  1. Add appropriate interactive elements (buttons/inputs) for each filter
  2. Implement ARIA attributes for accessibility
  3. Add hover/focus states for interactive elements

Example implementation for the search filter:

-          <div className="flex gap-2 items-center px-2">
-            <Magnifier className="text-accent-9 size-4" />
-            <span className="text-accent-12 font-medium text-[13px]">Search logs...</span>
-          </div>
+          <div className="flex gap-2 items-center px-2 relative">
+            <Magnifier className="text-accent-9 size-4 absolute left-4" />
+            <input
+              type="search"
+              placeholder="Search logs..."
+              aria-label="Search logs"
+              className="pl-10 pr-4 py-2 text-[13px] bg-transparent border border-gray-4 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-7"
+            />
+          </div>

Committable suggestion skipped: line range outside the PR's diff.

34 changes: 34 additions & 0 deletions apps/dashboard/app/(app)/logs-v2/components/logs-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import type { Log } from "@unkey/clickhouse/src/logs";
import { useCallback, useState } from "react";
import { LogsChart } from "./charts";
import { LogsFilters } from "./filters";
import { LogDetails } from "./table/log-details";
import { LogsTable } from "./table/logs-table";

export const LogsClient = () => {
const [selectedLog, setSelectedLog] = useState<Log | null>(null);
const [tableDistanceToTop, setTableDistanceToTop] = useState(0);

const handleDistanceToTop = useCallback((distanceToTop: number) => {
setTableDistanceToTop(distanceToTop);
}, []);

const handleLogSelection = useCallback((log: Log | null) => {
setSelectedLog(log);
}, []);

return (
<>
<LogsFilters />
<LogsChart onMount={handleDistanceToTop} />
<LogsTable onLogSelect={handleLogSelection} selectedLog={selectedLog} />
<LogDetails
log={selectedLog}
onClose={() => handleLogSelection(null)}
distanceToTop={tableDistanceToTop}
/>
</>
);
};
Loading