Skip to content

Commit

Permalink
feat: Logs v2 design phase (#2789)
Browse files Browse the repository at this point in the history
* feat: add new layout for logs page

* feat: add charts

* fix: import paths

* fix: colors

* refactor: colors of timestamp info

* feat: finish table rows

* feat: fix log details to chart

* refactor: clean up log details

* feat: add new log details header section

* refactor: improve feel of virtual table

* feat: finalize log details

* refactor: redundant null check

* refactor: adjust texts

* refactor: remove old virtual table and make it more readable

* feat: add new chart tooltip

* chore: fix build step

* chore: run formatter

* chore: run formatter

* feat: update icons

* fix: style issue due to virtual table changes

* [autofix.ci] apply automated fixes

* feat: add feature flog for logs-v2

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
ogzhanolguncu and autofix-ci[bot] authored Jan 8, 2025
1 parent 8a1ccc3 commit 2cb3b62
Show file tree
Hide file tree
Showing 46 changed files with 2,419 additions and 425 deletions.
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>
);
}
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

0 comments on commit 2cb3b62

Please sign in to comment.