Skip to content

Commit cae22ee

Browse files
committed
Improved error page layout
1 parent c5b966d commit cae22ee

File tree

2 files changed

+92
-77
lines changed
  • apps/webapp/app
    • components/navigation
    • routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint

2 files changed

+92
-77
lines changed

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ import { SideMenuHeader } from "./SideMenuHeader";
114114
import { SideMenuItem } from "./SideMenuItem";
115115
import { SideMenuSection } from "./SideMenuSection";
116116
import { type SideMenuSectionId } from "./sideMenuTypes";
117+
import { IconBugFilled } from "@tabler/icons-react";
117118

118119
/** Get the collapsed state for a specific side menu section from user preferences */
119120
function getSectionCollapsed(
@@ -478,7 +479,7 @@ export function SideMenu({
478479
)}
479480
<SideMenuItem
480481
name="Errors"
481-
icon={BugAntIcon}
482+
icon={IconBugFilled}
482483
activeIconColor="text-amber-500"
483484
inactiveIconColor="text-amber-500"
484485
to={v3ErrorsPath(organization, project, environment)}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx

Lines changed: 90 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { $replica } from "~/db.server";
1919
import { logsClickhouseClient, clickhouseClient } from "~/services/clickhouseInstance.server";
2020
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
2121
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
22-
import { Suspense } from "react";
22+
import { Suspense, useMemo } from "react";
2323
import { Spinner } from "~/components/primitives/Spinner";
2424
import { Paragraph } from "~/components/primitives/Paragraph";
2525
import { Callout } from "~/components/primitives/Callout";
@@ -28,18 +28,9 @@ import { formatDistanceToNow } from "date-fns";
2828
import { formatNumberCompact } from "~/utils/numberFormatter";
2929
import * as Property from "~/components/primitives/PropertyTable";
3030
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
31-
import { DateTime, formatDateTime } from "~/components/primitives/DateTime";
31+
import { DateTime } from "~/components/primitives/DateTime";
3232
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
33-
import {
34-
Bar,
35-
BarChart,
36-
ReferenceLine,
37-
ResponsiveContainer,
38-
Tooltip,
39-
YAxis,
40-
type TooltipProps,
41-
} from "recharts";
42-
import TooltipPortal from "~/components/primitives/TooltipPortal";
33+
import { Chart, type ChartConfig } from "~/components/primitives/charts/ChartCompound";
4334

4435
export const meta: MetaFunction<typeof loader> = ({ data }) => {
4536
return [
@@ -217,12 +208,12 @@ function ErrorGroupDetail({
217208
}
218209

219210
return (
220-
<div className="grid h-full grid-rows-[auto_auto_1fr] overflow-hidden">
211+
<div className="grid h-full grid-rows-[auto_16rem_1fr] overflow-hidden">
221212
{/* Error Summary */}
222213
<div className="border-b border-grid-bright p-4">
223214
<Header2 className="mb-4">{errorGroup.errorMessage}</Header2>
224215

225-
<div className="grid grid-cols-2 gap-x-12 gap-y-1">
216+
<div className="grid grid-cols-3 gap-x-12 gap-y-1">
226217
<Property.Table>
227218
<Property.Item>
228219
<Property.Label>ID</Property.Label>
@@ -243,6 +234,9 @@ function ErrorGroupDetail({
243234
<Property.Label>Occurrences</Property.Label>
244235
<Property.Value>{formatNumberCompact(errorGroup.count)}</Property.Value>
245236
</Property.Item>
237+
</Property.Table>
238+
239+
<Property.Table>
246240
<Property.Item>
247241
<Property.Label>First seen</Property.Label>
248242
<Property.Value>
@@ -271,13 +265,10 @@ function ErrorGroupDetail({
271265
</div>
272266

273267
{/* Activity over past 7 days by hour */}
274-
<div className="border-b border-grid-bright px-4 py-3">
275-
<Header3 className="mb-2">Activity (past 7 days)</Header3>
268+
<div className="flex flex-col overflow-hidden border-b border-grid-bright px-4 py-3">
269+
<Header3 className="mb-2 shrink-0">Activity (past 7 days)</Header3>
276270
<Suspense fallback={<ActivityChartBlankState />}>
277-
<TypedAwait
278-
resolve={hourlyActivity}
279-
errorElement={<ActivityChartBlankState />}
280-
>
271+
<TypedAwait resolve={hourlyActivity} errorElement={<ActivityChartBlankState />}>
281272
{(activity) =>
282273
activity.length > 0 ? (
283274
<ActivityChart activity={activity} />
@@ -291,7 +282,7 @@ function ErrorGroupDetail({
291282

292283
{/* Runs Table */}
293284
<div className="flex flex-col gap-1 overflow-y-hidden">
294-
<Header3 className="mt-2 mb-1 px-4">Recent runs</Header3>
285+
<Header3 className="mb-1 mt-2 px-4">Recent runs</Header3>
295286
{runList ? (
296287
<TaskRunsTable
297288
total={runList.runs.length}
@@ -318,69 +309,92 @@ function ErrorGroupDetail({
318309
);
319310
}
320311

321-
function ActivityChart({ activity }: { activity: ErrorGroupHourlyActivity }) {
322-
const maxCount = Math.max(...activity.map((d) => d.count));
312+
const activityChartConfig: ChartConfig = {
313+
count: {
314+
label: "Occurrences",
315+
color: "#EC003F",
316+
},
317+
};
323318

324-
return (
325-
<div className="flex items-start gap-2">
326-
<div className="h-16 flex-1 rounded-sm">
327-
<ResponsiveContainer width="100%" height="100%">
328-
<BarChart data={activity} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
329-
<YAxis domain={[0, maxCount || 1]} hide />
330-
<Tooltip
331-
cursor={{ fill: "transparent" }}
332-
content={<ActivityChartTooltip />}
333-
allowEscapeViewBox={{ x: true, y: true }}
334-
wrapperStyle={{ zIndex: 1000 }}
335-
animationDuration={0}
336-
/>
337-
<Bar dataKey="count" fill="#EC003F" strokeWidth={0} isAnimationActive={false} />
338-
<ReferenceLine y={0} stroke="#B5B8C0" strokeWidth={1} />
339-
{maxCount > 0 && (
340-
<ReferenceLine
341-
y={maxCount}
342-
stroke="#B5B8C0"
343-
strokeDasharray="3 2"
344-
strokeWidth={1}
345-
/>
346-
)}
347-
</BarChart>
348-
</ResponsiveContainer>
349-
</div>
350-
<span className="text-xxs tabular-nums text-text-dimmed">
351-
{formatNumberCompact(maxCount)}
352-
</span>
353-
</div>
319+
function ActivityChart({ activity }: { activity: ErrorGroupHourlyActivity }) {
320+
const data = useMemo(
321+
() =>
322+
activity.map((d) => ({
323+
...d,
324+
__timestamp: d.date instanceof Date ? d.date.getTime() : new Date(d.date).getTime(),
325+
})),
326+
[activity]
354327
);
355-
}
356328

357-
const ActivityChartTooltip = ({ active, payload }: TooltipProps<number, string>) => {
358-
if (active && payload && payload.length > 0) {
359-
const entry = payload[0].payload as { date: Date; count: number };
360-
const date = entry.date instanceof Date ? entry.date : new Date(entry.date);
361-
const formattedDate = formatDateTime(date, "UTC", [], false, true);
329+
const midnightTicks = useMemo(() => {
330+
const ticks: number[] = [];
331+
for (const d of data) {
332+
const date = new Date(d.__timestamp);
333+
if (date.getHours() === 0 && date.getMinutes() === 0) {
334+
ticks.push(d.__timestamp);
335+
}
336+
}
337+
return ticks;
338+
}, [data]);
362339

363-
return (
364-
<TooltipPortal active={active}>
365-
<div className="rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2">
366-
<Header3 className="border-b border-b-charcoal-650 pb-2">{formattedDate}</Header3>
367-
<div className="mt-2 text-xs text-text-bright">
368-
<span className="tabular-nums">{entry.count}</span>{" "}
369-
<span className="text-text-dimmed">
370-
{entry.count === 1 ? "occurrence" : "occurrences"}
371-
</span>
372-
</div>
373-
</div>
374-
</TooltipPortal>
375-
);
376-
}
340+
const xAxisFormatter = useMemo(() => {
341+
return (value: number) => {
342+
const date = new Date(value);
343+
return date.toLocaleDateString("en-US", {
344+
month: "short",
345+
day: "numeric",
346+
hour: "2-digit",
347+
minute: "2-digit",
348+
hour12: false,
349+
});
350+
};
351+
}, []);
377352

378-
return null;
379-
};
353+
const tooltipLabelFormatter = useMemo(() => {
354+
return (_label: string, payload: Array<{ payload?: Record<string, unknown> }>) => {
355+
const timestamp = payload[0]?.payload?.__timestamp as number | undefined;
356+
if (timestamp) {
357+
const date = new Date(timestamp);
358+
return date.toLocaleString("en-US", {
359+
month: "short",
360+
day: "numeric",
361+
year: "numeric",
362+
hour: "2-digit",
363+
minute: "2-digit",
364+
hour12: false,
365+
});
366+
}
367+
return _label;
368+
};
369+
}, []);
370+
371+
return (
372+
<Chart.Root
373+
config={activityChartConfig}
374+
data={data}
375+
dataKey="__timestamp"
376+
series={["count"]}
377+
fillContainer
378+
>
379+
<Chart.Bar
380+
xAxisProps={{
381+
tickFormatter: xAxisFormatter,
382+
ticks: midnightTicks,
383+
height: 40,
384+
}}
385+
yAxisProps={{
386+
width: 30,
387+
tickMargin: 4,
388+
}}
389+
tooltipLabelFormatter={tooltipLabelFormatter}
390+
/>
391+
</Chart.Root>
392+
);
393+
}
380394

381395
function ActivityChartBlankState() {
382396
return (
383-
<div className="flex h-16 w-full items-end gap-px rounded-sm">
397+
<div className="flex min-h-0 flex-1 items-end gap-px rounded-sm">
384398
{[...Array(42)].map((_, i) => (
385399
<div key={i} className="h-full flex-1 bg-charcoal-850" />
386400
))}

0 commit comments

Comments
 (0)