Skip to content

Commit 5cdda2e

Browse files
chore: replace date-fns by dayjs (coder#18022)
This change replaces date-fns with dayjs throughout the codebase for more consistent date/time handling and to reduce bundle size. It also tries to make the formatting and usage consistent. **Why dayjs over date-fns?** Just because we were using dayjs more broadly. Its formatting capabilities, were also easier to extend.
1 parent a605c09 commit 5cdda2e

File tree

20 files changed

+241
-178
lines changed

20 files changed

+241
-178
lines changed

site/e2e/api.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import type { Page } from "@playwright/test";
22
import { expect } from "@playwright/test";
33
import { API, type DeploymentConfig } from "api/api";
44
import type { SerpentOption } from "api/typesGenerated";
5-
import { formatDuration, intervalToDuration } from "date-fns";
5+
import dayjs from "dayjs";
6+
import duration from "dayjs/plugin/duration";
7+
import relativeTime from "dayjs/plugin/relativeTime";
8+
9+
dayjs.extend(duration);
10+
dayjs.extend(relativeTime);
11+
import { humanDuration } from "utils/time";
612
import { coderPort, defaultPassword } from "./constants";
713
import { type LoginOptions, findSessionToken, randomName } from "./helpers";
814

@@ -237,13 +243,6 @@ export async function verifyConfigFlagString(
237243
await expect(configOption).toHaveText(opt.value as any);
238244
}
239245

240-
export async function verifyConfigFlagEmpty(page: Page, flag: string) {
241-
const configOption = page.locator(
242-
`div.options-table .option-${flag} .option-value-empty`,
243-
);
244-
await expect(configOption).toHaveText("Not set");
245-
}
246-
247246
export async function verifyConfigFlagArray(
248247
page: Page,
249248
config: DeploymentConfig,
@@ -290,19 +289,15 @@ export async function verifyConfigFlagDuration(
290289
flag: string,
291290
) {
292291
const opt = findConfigOption(config, flag);
292+
if (typeof opt.value !== "number") {
293+
throw new Error(
294+
`Option with env ${flag} should be a number, but got ${typeof opt.value}.`,
295+
);
296+
}
293297
const configOption = page.locator(
294298
`div.options-table .option-${flag} .option-value-string`,
295299
);
296-
//
297-
await expect(configOption).toHaveText(
298-
formatDuration(
299-
// intervalToDuration takes ms, so convert nanoseconds to ms
300-
intervalToDuration({
301-
start: 0,
302-
end: (opt.value as number) / 1e6,
303-
}),
304-
),
305-
);
300+
await expect(configOption).toHaveText(humanDuration(opt.value / 1e6));
306301
}
307302

308303
export function findConfigOption(

site/e2e/tests/deployment/observability.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
verifyConfigFlagArray,
66
verifyConfigFlagBoolean,
77
verifyConfigFlagDuration,
8-
verifyConfigFlagEmpty,
98
verifyConfigFlagString,
109
} from "../../api";
1110
import { login } from "../../helpers";
@@ -28,7 +27,11 @@ test("enabled observability settings", async ({ page }) => {
2827
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
2928
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
3029
await verifyConfigFlagDuration(page, config, "health-check-refresh");
31-
await verifyConfigFlagEmpty(page, "health-check-threshold-database");
30+
await verifyConfigFlagDuration(
31+
page,
32+
config,
33+
"health-check-threshold-database",
34+
);
3235
await verifyConfigFlagString(page, config, "log-human");
3336
await verifyConfigFlagString(page, config, "prometheus-address");
3437
await verifyConfigFlagArray(

site/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@
8585
"color-convert": "2.0.1",
8686
"cron-parser": "4.9.0",
8787
"cronstrue": "2.50.0",
88-
"date-fns": "2.30.0",
8988
"dayjs": "1.11.13",
9089
"emoji-mart": "5.6.0",
9190
"file-saver": "2.0.5",
9291
"formik": "2.4.6",
9392
"front-matter": "4.0.2",
93+
"humanize-duration": "3.32.2",
9494
"jszip": "3.10.1",
9595
"lodash": "4.17.21",
9696
"lucide-react": "0.474.0",
@@ -149,6 +149,7 @@
149149
"@types/color-convert": "2.0.4",
150150
"@types/express": "4.17.17",
151151
"@types/file-saver": "2.0.7",
152+
"@types/humanize-duration": "3.27.4",
152153
"@types/jest": "29.5.14",
153154
"@types/lodash": "4.17.15",
154155
"@types/node": "20.17.16",

site/pnpm-lock.yaml

Lines changed: 16 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/components/LastSeen/LastSeen.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { useTheme } from "@emotion/react";
22
import dayjs from "dayjs";
3-
import relativeTime from "dayjs/plugin/relativeTime";
43
import type { FC, HTMLAttributes } from "react";
54
import { cn } from "utils/cn";
6-
7-
dayjs.extend(relativeTime);
5+
import { isAfter, relativeTime, subtractTime } from "utils/time";
86

97
interface LastSeenProps
108
extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
@@ -15,21 +13,25 @@ interface LastSeenProps
1513
export const LastSeen: FC<LastSeenProps> = ({ at, className, ...attrs }) => {
1614
const theme = useTheme();
1715
const t = dayjs(at);
18-
const now = dayjs();
16+
const now = new Date();
17+
const oneHourAgo = subtractTime(now, 1, "hour");
18+
const threeDaysAgo = subtractTime(now, 3, "day");
19+
const oneMonthAgo = subtractTime(now, 1, "month");
20+
const centuryAgo = subtractTime(now, 100, "year");
1921

20-
let message = t.fromNow();
22+
let message = relativeTime(at);
2123
let color = theme.palette.text.secondary;
2224

23-
if (t.isAfter(now.subtract(1, "hour"))) {
25+
if (isAfter(at, oneHourAgo)) {
2426
// Since the agent reports on a 10m interval,
2527
// the last_used_at can be inaccurate when recent.
2628
message = "Now";
2729
color = theme.roles.success.fill.solid;
28-
} else if (t.isAfter(now.subtract(3, "day"))) {
30+
} else if (isAfter(at, threeDaysAgo)) {
2931
color = theme.experimental.l2.text;
30-
} else if (t.isAfter(now.subtract(1, "month"))) {
32+
} else if (isAfter(at, oneMonthAgo)) {
3133
color = theme.roles.warning.fill.solid;
32-
} else if (t.isAfter(now.subtract(100, "year"))) {
34+
} else if (isAfter(at, centuryAgo)) {
3335
color = theme.roles.error.fill.solid;
3436
} else {
3537
message = "Never";

site/src/components/Timeline/utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import formatRelative from "date-fns/formatRelative";
2-
import subDays from "date-fns/subDays";
1+
import dayjs from "dayjs";
2+
import calendar from "dayjs/plugin/calendar";
3+
4+
dayjs.extend(calendar);
35

46
export const createDisplayDate = (
57
date: Date,
68
base: Date = new Date(),
79
): string => {
8-
const lastWeek = subDays(base, 7);
10+
const lastWeek = dayjs(base).subtract(7, "day").toDate();
911
if (date >= lastWeek) {
10-
return formatRelative(date, base).split(" at ")[0];
12+
return dayjs(date).calendar(dayjs(base), {
13+
sameDay: "[Today]",
14+
lastDay: "[Yesterday]",
15+
lastWeek: "[last] dddd",
16+
sameElse: "MM/DD/YYYY",
17+
});
1118
}
1219
return date.toLocaleDateString();
1320
};

site/src/modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import Tooltip from "@mui/material/Tooltip";
22
import type { Workspace } from "api/typesGenerated";
33
import { Badge } from "components/Badge/Badge";
4-
import { formatDistanceToNow } from "date-fns";
54
import type { FC } from "react";
5+
import {
6+
DATE_FORMAT,
7+
formatDateTime,
8+
relativeTimeWithoutSuffix,
9+
} from "utils/time";
610

711
export type WorkspaceDormantBadgeProps = {
812
workspace: Workspace;
@@ -11,25 +15,14 @@ export type WorkspaceDormantBadgeProps = {
1115
export const WorkspaceDormantBadge: FC<WorkspaceDormantBadgeProps> = ({
1216
workspace,
1317
}) => {
14-
const formatDate = (dateStr: string): string => {
15-
const date = new Date(dateStr);
16-
return date.toLocaleDateString(undefined, {
17-
month: "long",
18-
day: "numeric",
19-
year: "numeric",
20-
hour: "numeric",
21-
minute: "numeric",
22-
});
23-
};
24-
2518
return workspace.deleting_at ? (
2619
<Tooltip
2720
title={
2821
<>
2922
This workspace has not been used for{" "}
30-
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and has been
23+
{relativeTimeWithoutSuffix(workspace.last_used_at)} and has been
3124
marked dormant. It is scheduled to be deleted on{" "}
32-
{formatDate(workspace.deleting_at)}.
25+
{formatDateTime(workspace.deleting_at, DATE_FORMAT.FULL_DATETIME)}.
3326
</>
3427
}
3528
>
@@ -42,7 +35,7 @@ export const WorkspaceDormantBadge: FC<WorkspaceDormantBadgeProps> = ({
4235
title={
4336
<>
4437
This workspace has not been used for{" "}
45-
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and has been
38+
{relativeTimeWithoutSuffix(workspace.last_used_at)} and has been
4639
marked dormant. It is not scheduled for auto-deletion but will become
4740
a candidate if auto-deletion is enabled on this template.
4841
</>

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { GetLicensesResponse } from "api/api";
55
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
66
import { Pill } from "components/Pill/Pill";
77
import { Stack } from "components/Stack/Stack";
8-
import { compareAsc } from "date-fns";
98
import dayjs from "dayjs";
109
import { type FC, useState } from "react";
1110

@@ -92,10 +91,7 @@ export const LicenseCard: FC<LicenseCardProps> = ({
9291
alignItems="center"
9392
width="134px" // standardize width of date column
9493
>
95-
{compareAsc(
96-
new Date(license.claims.license_expires * 1000),
97-
new Date(),
98-
) < 1 ? (
94+
{dayjs(license.claims.license_expires * 1000).isBefore(dayjs()) ? (
9995
<Pill css={styles.expiredBadge} type="error">
10096
Expired
10197
</Pill>

site/src/pages/DeploymentSettingsPage/optionValue.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SerpentOption } from "api/typesGenerated";
2-
import { formatDuration, intervalToDuration } from "date-fns";
2+
import { humanDuration } from "utils/time";
33

44
// optionValue is a helper function to format the value of a specific deployment options
55
export function optionValue(
@@ -14,13 +14,7 @@ export function optionValue(
1414
}
1515
switch (k) {
1616
case "format_duration":
17-
return formatDuration(
18-
// intervalToDuration takes ms, so convert nanoseconds to ms
19-
intervalToDuration({
20-
start: 0,
21-
end: (option.value as number) / 1e6,
22-
}),
23-
);
17+
return humanDuration((option.value as number) / 1e6);
2418
// Add additional cases here as needed.
2519
}
2620
}

0 commit comments

Comments
 (0)