Files
flxn-app/src/features/telemetry/components/sessions/session-timeline.tsx
T

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;