106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
import { Fragment } from "react";
|
|
import { Box, Group, Stack, Text } from "@mantine/core";
|
|
import { Plural, useLingui } from "@lingui/react/macro";
|
|
import { useSessionTimeline } from "../../queries";
|
|
import type { TimelineEntry } from "../../types";
|
|
|
|
const GAP_MARKER_MS = 5 * 60 * 1000;
|
|
|
|
const parseTs = (ts: string) => Date.parse(ts.replace(" ", "T"));
|
|
|
|
const dotColor = (entry: TimelineEntry): string => {
|
|
if (entry.kind === "error" || entry.success === false) return "var(--mantine-color-red-6)";
|
|
if (entry.kind === "server_fn") return "var(--mantine-color-green-6)";
|
|
return "var(--mantine-color-dimmed)";
|
|
};
|
|
|
|
const Dot = ({ color }: { color: string }) => (
|
|
<Box
|
|
w={6}
|
|
h={6}
|
|
style={{ borderRadius: "50%", backgroundColor: color, flexShrink: 0 }}
|
|
/>
|
|
);
|
|
|
|
export const SessionTimelineView = ({ sessionId }: { sessionId: string }) => {
|
|
const { i18n } = useLingui();
|
|
const { data } = useSessionTimeline(sessionId);
|
|
|
|
const clock = (ts: string) =>
|
|
i18n.date(new Date(parseTs(ts)), { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
|
|
return (
|
|
<Stack gap={0}>
|
|
<Group gap="md" pb="sm">
|
|
<Text size="xs" c="dimmed">
|
|
{i18n.date(new Date(parseTs(data.session.started)), {
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
<Plural value={data.session.pageViews} one="# page" other="# pages" />
|
|
</Text>
|
|
{data.session.errors > 0 && (
|
|
<Text size="xs" c="red">
|
|
<Plural value={data.session.errors} one="# error" other="# errors" />
|
|
</Text>
|
|
)}
|
|
</Group>
|
|
|
|
{data.entries.map((entry, index) => {
|
|
const prev = data.entries[index - 1];
|
|
const gapMs = prev ? parseTs(entry.ts) - parseTs(prev.ts) : 0;
|
|
return (
|
|
<Fragment key={`${entry.ts}-${index}`}>
|
|
{gapMs > GAP_MARKER_MS && (
|
|
<Text size="xs" c="dimmed" py={4} pl={46}>
|
|
+{Math.round(gapMs / 60000)}m
|
|
</Text>
|
|
)}
|
|
<Group gap="xs" py={5} wrap="nowrap" align="center">
|
|
<Text
|
|
size="xs"
|
|
c="dimmed"
|
|
w={64}
|
|
style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}
|
|
>
|
|
{clock(entry.ts)}
|
|
</Text>
|
|
<Dot color={dotColor(entry)} />
|
|
<Text
|
|
size="sm"
|
|
lineClamp={1}
|
|
c={entry.kind === "error" ? "red" : undefined}
|
|
style={{ minWidth: 0, flex: 1 }}
|
|
>
|
|
{entry.label}
|
|
{entry.detail && entry.kind !== "error" && (
|
|
<Text component="span" size="sm" c="dimmed">
|
|
{" "}
|
|
· {entry.detail}
|
|
</Text>
|
|
)}
|
|
</Text>
|
|
{entry.kind === "server_fn" && entry.success === false && (
|
|
<Text size="xs" c="red" style={{ flexShrink: 0 }}>
|
|
✗
|
|
</Text>
|
|
)}
|
|
{entry.kind === "server_fn" && typeof entry.duration === "number" && entry.duration > 0 && (
|
|
<Text size="xs" c="dimmed" style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>
|
|
{entry.duration}ms
|
|
</Text>
|
|
)}
|
|
</Group>
|
|
</Fragment>
|
|
);
|
|
})}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export default SessionTimelineView;
|