Skip to content

Commit 21037f2

Browse files
Replace outdated react-time-ago (#8255)
1 parent c7ff5bb commit 21037f2

9 files changed

Lines changed: 132 additions & 82 deletions

File tree

packages/web/app/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"@hookform/resolvers": "3.10.0",
3535
"@ladle/react": "5.1.1",
3636
"@monaco-editor/react": "4.8.0-rc.2",
37-
"@n1ru4l/react-time-ago": "1.1.0",
3837
"@pierre/diffs": "1.2.3",
3938
"@radix-ui/react-accordion": "1.2.2",
4039
"@radix-ui/react-alert-dialog": "1.1.4",

packages/web/app/src/components/target/alerts/alert-activity-table.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { formatDistanceToNow } from 'date-fns';
21
import { ArrowRight } from 'lucide-react';
32
import { DataTable } from '@/components/base/data-table/data-table';
43
import { BadgeRounded } from '@/components/ui/badge';
4+
import { TimeAgo } from '@/components/ui/time-ago';
55
import { Avatar } from '@/components/v2/avatar';
66
import {
77
MetricAlertRuleType,
@@ -72,9 +72,10 @@ const COLUMNS = [
7272
id: 'age',
7373
header: 'Age',
7474
cell: ctx => (
75-
<span className="text-neutral-12 inline-flex items-center gap-1 font-mono text-[11px]">
76-
{formatDistanceToNow(new Date(ctx.row.original.createdAt), { addSuffix: true })}
77-
</span>
75+
<TimeAgo
76+
date={ctx.row.original.createdAt}
77+
className="text-neutral-12 font-mono text-[11px]"
78+
/>
7879
),
7980
}),
8081
columnHelper.display({

packages/web/app/src/components/target/alerts/alert-conditions-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useState } from 'react';
2-
import { formatDistanceToNow } from 'date-fns';
32
import { ExternalLink, Info } from 'lucide-react';
43
import { Button } from '@/components/base/button/button';
54
import { DescriptionList } from '@/components/base/description-list/description-list';
@@ -13,6 +12,7 @@ import {
1312
SheetHeader,
1413
SheetTitle,
1514
} from '@/components/ui/sheet';
15+
import { TimeAgo } from '@/components/ui/time-ago';
1616
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
1717
import { Avatar } from '@/components/v2/avatar';
1818
import {
@@ -135,7 +135,7 @@ export type AlertConditionsPanelProps = {
135135
function RelativeTimestamp({ iso }: { iso: string }) {
136136
return (
137137
<span className="text-neutral-12 inline-flex items-center gap-1 font-mono text-[10px]">
138-
{formatDistanceToNow(new Date(iso), { addSuffix: true })}
138+
<TimeAgo date={iso} />
139139
<TooltipProvider delayDuration={100}>
140140
<Tooltip>
141141
<TooltipTrigger asChild>

packages/web/app/src/components/target/alerts/alert-events-table.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { formatDistanceToNow } from 'date-fns';
21
import { ArrowRight } from 'lucide-react';
32
import { DataTable } from '@/components/base/data-table/data-table';
3+
import { TimeAgo } from '@/components/ui/time-ago';
44
import { type MetricAlertRuleState, type MetricAlertRuleType } from '@/gql/graphql';
55
import { createColumnHelper } from '@tanstack/react-table';
66
import {
@@ -53,9 +53,10 @@ const COLUMNS = [
5353
id: 'age',
5454
header: 'Age',
5555
cell: ctx => (
56-
<span className="text-neutral-12 inline-flex items-center gap-1 font-mono text-[11px]">
57-
{formatDistanceToNow(new Date(ctx.row.original.createdAt), { addSuffix: true })}
58-
</span>
56+
<TimeAgo
57+
date={ctx.row.original.createdAt}
58+
className="text-neutral-12 font-mono text-[11px]"
59+
/>
5960
),
6061
}),
6162
columnHelper.display({
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { formatTimeAgo } from './time-ago';
2+
3+
const SECOND = 1000;
4+
const MINUTE = 60 * SECOND;
5+
const HOUR = 60 * MINUTE;
6+
const DAY = 24 * HOUR;
7+
const WEEK = 7 * DAY;
8+
const MONTH = 30 * DAY;
9+
const YEAR = 365 * DAY;
10+
11+
// Fixed reference "now"; every case is `now` minus some elapsed time.
12+
const NOW = new Date('2026-07-23T12:00:00.000Z').getTime();
13+
const ago = (elapsedMs: number) => formatTimeAgo(new Date(NOW - elapsedMs), NOW);
14+
15+
describe('formatTimeAgo', () => {
16+
it('shows "now" under 2 minutes', () => {
17+
expect(ago(0)).toBe('now');
18+
expect(ago(30 * SECOND)).toBe('now');
19+
expect(ago(2 * MINUTE - SECOND)).toBe('now');
20+
});
21+
22+
it('shows minutes from 2m up to an hour', () => {
23+
expect(ago(2 * MINUTE)).toBe('2m ago');
24+
expect(ago(5 * MINUTE)).toBe('5m ago');
25+
expect(ago(HOUR - SECOND)).toBe('59m ago');
26+
});
27+
28+
it('shows hours from 1h up to a day', () => {
29+
expect(ago(HOUR)).toBe('1h ago');
30+
expect(ago(3 * HOUR)).toBe('3h ago');
31+
expect(ago(DAY - SECOND)).toBe('23h ago');
32+
});
33+
34+
it('shows days from 1d up to a week', () => {
35+
expect(ago(DAY)).toBe('1d ago');
36+
expect(ago(6 * DAY)).toBe('6d ago');
37+
expect(ago(WEEK - SECOND)).toBe('6d ago');
38+
});
39+
40+
it('rolls up to weeks from 1w up to a month', () => {
41+
expect(ago(WEEK)).toBe('1w ago');
42+
expect(ago(3 * WEEK)).toBe('3w ago');
43+
expect(ago(MONTH - SECOND)).toBe('4w ago');
44+
});
45+
46+
it('rolls up to months from 1mo up to a year', () => {
47+
expect(ago(MONTH)).toBe('1mo ago');
48+
expect(ago(2 * MONTH)).toBe('2mo ago');
49+
expect(ago(YEAR - SECOND)).toBe('12mo ago');
50+
});
51+
52+
it('rolls up to years past a year, so old dates stay readable', () => {
53+
expect(ago(YEAR)).toBe('1y ago');
54+
expect(ago(Math.floor(2.5 * YEAR))).toBe('2y ago');
55+
expect(ago(5 * YEAR)).toBe('5y ago');
56+
});
57+
});
Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,45 @@
1-
import { ReactElement, useMemo } from 'react';
1+
import { ReactElement, useEffect, useMemo, useState } from 'react';
22
import clsx from 'clsx';
33
import { format } from 'date-fns';
4-
import { TimeAgo as ReactTimeAgo } from '@n1ru4l/react-time-ago';
4+
5+
const MINUTE = 60;
6+
const HOUR = MINUTE * 60;
7+
const DAY = HOUR * 24;
8+
const WEEK = DAY * 7;
9+
const MONTH = DAY * 30;
10+
const YEAR = DAY * 365;
11+
12+
// Terse relative label ("now" / "5m ago" / "3h ago" / "12d ago"), matching the
13+
// old @n1ru4l/react-time-ago look, but rolling up past days into w/mo/y so old
14+
// dates stay readable (e.g. "2y ago" instead of "912d ago"). Months/years use
15+
// approximate 30/365-day buckets, which is fine at this granularity.
16+
export function formatTimeAgo(dateObj: Date, now: number): string {
17+
const d = (now - dateObj.getTime()) / 1000;
18+
if (d < MINUTE * 2) {
19+
return 'now';
20+
}
21+
if (d < HOUR) {
22+
return `${Math.floor(d / MINUTE)}m ago`;
23+
}
24+
if (d < DAY) {
25+
return `${Math.floor(d / HOUR)}h ago`;
26+
}
27+
if (d < WEEK) {
28+
return `${Math.floor(d / DAY)}d ago`;
29+
}
30+
if (d < MONTH) {
31+
return `${Math.floor(d / WEEK)}w ago`;
32+
}
33+
if (d < YEAR) {
34+
return `${Math.floor(d / MONTH)}mo ago`;
35+
}
36+
return `${Math.floor(d / YEAR)}y ago`;
37+
}
38+
39+
// Re-render cadence for the live label. A fixed 30s tick keeps the
40+
// minute-granular output feeling live without churning (the finest unit is
41+
// minutes, so 30s always refreshes at least as often as the label can change).
42+
const REFRESH_INTERVAL_MS = 30_000;
543

644
export const TimeAgo = ({
745
date,
@@ -10,6 +48,8 @@ export const TimeAgo = ({
1048
date?: string;
1149
className?: string;
1250
}): ReactElement | null => {
51+
const [now, setNow] = useState(() => Date.now());
52+
1353
const { dateObj, formattedDate } = useMemo(() => {
1454
if (!date) {
1555
return {};
@@ -19,21 +59,25 @@ export const TimeAgo = ({
1959
return { dateObj, formattedDate };
2060
}, [date]);
2161

62+
useEffect(() => {
63+
if (!dateObj) {
64+
return;
65+
}
66+
const id = setInterval(() => setNow(Date.now()), REFRESH_INTERVAL_MS);
67+
return () => clearInterval(id);
68+
}, [dateObj]);
69+
2270
if (!date || !dateObj) {
2371
return null;
2472
}
2573

2674
return (
27-
<ReactTimeAgo date={dateObj}>
28-
{({ value }) => (
29-
<time
30-
dateTime={formattedDate}
31-
title={formattedDate}
32-
className={clsx('cursor-default whitespace-nowrap', className)}
33-
>
34-
{value}
35-
</time>
36-
)}
37-
</ReactTimeAgo>
75+
<time
76+
dateTime={formattedDate}
77+
title={formattedDate}
78+
className={clsx('cursor-default whitespace-nowrap', className)}
79+
>
80+
{formatTimeAgo(dateObj, now)}
81+
</time>
3882
);
3983
};

packages/web/app/src/pages/target-alerts-rules.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { useMemo } from 'react';
2-
import { formatDistanceToNow } from 'date-fns';
32
import { ArrowDown, Info } from 'lucide-react';
43
import { useQuery } from 'urql';
54
import { DataTable } from '@/components/base/data-table/data-table';
65
import { PageLead } from '@/components/base/page-lead';
76
import { Badge, BadgeRounded } from '@/components/ui/badge';
87
import { Spinner } from '@/components/ui/spinner';
8+
import { TimeAgo } from '@/components/ui/time-ago';
99
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
1010
import { Avatar } from '@/components/v2/avatar';
1111
import { graphql } from '@/gql';
@@ -130,11 +130,6 @@ function destinationLabel(channels: ReadonlyArray<{ type: string }>): string {
130130
.join(', ');
131131
}
132132

133-
function relativeTime(iso?: string | null): string {
134-
if (!iso) return '—';
135-
return formatDistanceToNow(new Date(iso), { addSuffix: true });
136-
}
137-
138133
function SortableHeader({ column, label }: { column: Column<RuleRow, unknown>; label: string }) {
139134
const sort = column.getIsSorted();
140135
const arrowOpacity = sort ? 'opacity-100' : 'opacity-30';
@@ -195,7 +190,9 @@ const RULE_COLUMNS: ColumnDef<RuleRow, any>[] = [
195190
return av - bv;
196191
},
197192
cell: info => (
198-
<span className="text-neutral-11 font-mono text-xs">{relativeTime(info.getValue())}</span>
193+
<span className="text-neutral-11 font-mono text-xs">
194+
{info.getValue() ? <TimeAgo date={info.getValue()!} /> : '—'}
195+
</span>
199196
),
200197
}),
201198
columnHelper.accessor('updatedAt', {
@@ -204,7 +201,7 @@ const RULE_COLUMNS: ColumnDef<RuleRow, any>[] = [
204201
new Date(a.original.updatedAt).getTime() - new Date(b.original.updatedAt).getTime(),
205202
cell: info => (
206203
<span className="text-neutral-11 inline-block min-w-[180px] whitespace-nowrap font-mono text-xs">
207-
{relativeTime(info.getValue())}
204+
{info.getValue() ? <TimeAgo date={info.getValue()} /> : '—'}
208205
</span>
209206
),
210207
}),

packages/web/app/src/vite-env.d.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,3 @@
22
/// <reference types="../.api/types.d.ts" />
33

44
declare module 'tailwindcss/colors';
5-
6-
declare module '@n1ru4l/react-time-ago' {
7-
export function TimeAgo(props: {
8-
date: Date;
9-
children: (args: { value: string }) => React.ReactElement;
10-
}): React.ReactElement;
11-
}

pnpm-lock.yaml

Lines changed: 1 addition & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)