Skip to content

Commit 3c252db

Browse files
committed
Fixes #4844 with more defensive date parsing
1 parent 8eba319 commit 3c252db

File tree

9 files changed

+37
-33
lines changed

9 files changed

+37
-33
lines changed

frontend/src/components/Form/AccessField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
44
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
55
import type { AccessList } from "src/api/backend";
66
import { useAccessLists } from "src/hooks";
7-
import { DateTimeFormat, intl, T } from "src/locale";
7+
import { formatDateTime, intl, T } from "src/locale";
88

99
interface AccessOption {
1010
readonly value: number;
@@ -48,7 +48,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
4848
{
4949
users: item?.items?.length,
5050
rules: item?.clients?.length,
51-
date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A",
51+
date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A",
5252
},
5353
),
5454
icon: <IconLock size={14} className="text-lime" />,

frontend/src/components/Form/SSLCertificateField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik";
33
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
44
import type { Certificate } from "src/api/backend";
55
import { useCertificates } from "src/hooks";
6-
import { DateTimeFormat, intl, T } from "src/locale";
6+
import { formatDateTime, intl, T } from "src/locale";
77

88
interface CertOption {
99
readonly value: number | "new";
@@ -75,7 +75,7 @@ export function SSLCertificateField({
7575
data?.map((cert: Certificate) => ({
7676
value: cert.id,
7777
label: cert.niceName,
78-
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} &mdash; ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A" })}`,
78+
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`,
7979
icon: <IconShield size={14} className="text-pink" />,
8080
})) || [];
8181

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import cn from "classnames";
2-
import { differenceInDays, isPast, parseISO } from "date-fns";
3-
import { DateTimeFormat } from "src/locale";
2+
import { differenceInDays, isPast } from "date-fns";
3+
import { formatDateTime, parseDate } from "src/locale";
44

55
interface Props {
66
value: string;
77
highlightPast?: boolean;
88
highlistNearlyExpired?: boolean;
99
}
1010
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
11-
const dateIsPast = isPast(parseISO(value));
12-
const days = differenceInDays(parseISO(value), new Date());
11+
const d = parseDate(value);
12+
const dateIsPast = d ? isPast(d) : false;
13+
const days = d ? differenceInDays(d, new Date()) : 0;
1314
const cl = cn({
1415
"text-danger": highlightPast && dateIsPast,
1516
"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
1617
});
17-
return <span className={cl}>{DateTimeFormat(value)}</span>;
18+
return <span className={cl}>{formatDateTime(value)}</span>;
1819
}

frontend/src/components/Table/Formatter/DomainsFormatter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import cn from "classnames";
22
import type { ReactNode } from "react";
3-
import { DateTimeFormat, T } from "src/locale";
3+
import { formatDateTime, T } from "src/locale";
44

55
interface Props {
66
domains: string[];
@@ -53,7 +53,7 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
5353
<div className="font-weight-medium">{...elms}</div>
5454
{createdOn ? (
5555
<div className="text-secondary mt-1">
56-
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
56+
<T id="created-on" data={{ date: formatDateTime(createdOn) }} />
5757
</div>
5858
) : null}
5959
</div>

frontend/src/components/Table/Formatter/EventFormatter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
22
import cn from "classnames";
33
import type { AuditLog } from "src/api/backend";
4-
import { DateTimeFormat, T } from "src/locale";
4+
import { formatDateTime, T } from "src/locale";
55

66
const getEventValue = (event: AuditLog) => {
77
switch (event.objectType) {
@@ -73,7 +73,7 @@ export function EventFormatter({ row }: Props) {
7373
<T id={`object.event.${row.action}`} tData={{ object: row.objectType }} />
7474
&nbsp; &mdash; <span className="badge">{getEventValue(row)}</span>
7575
</div>
76-
<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div>
76+
<div className="text-secondary mt-1">{formatDateTime(row.createdOn)}</div>
7777
</div>
7878
);
7979
}

frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DateTimeFormat, T } from "src/locale";
1+
import { formatDateTime, T } from "src/locale";
22

33
interface Props {
44
value: string;
@@ -13,7 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
1313
</div>
1414
{createdOn ? (
1515
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
16-
<T id={disabled ? "disabled" : "created-on"} data={{ date: DateTimeFormat(createdOn) }} />
16+
<T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn) }} />
1717
</div>
1818
) : null}
1919
</div>
Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DateTimeFormat } from "src/locale";
1+
import { formatDateTime } from "src/locale";
22
import { afterAll, beforeAll, describe, expect, it } from "vitest";
33

44
describe("DateFormatter", () => {
@@ -17,10 +17,7 @@ describe("DateFormatter", () => {
1717

1818
// Mock Intl.DateTimeFormat so formatting is stable regardless of host
1919
const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {
20-
constructor(
21-
_locales?: string | string[],
22-
options?: Intl.DateTimeFormatOptions,
23-
) {
20+
constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) {
2421
super(desiredLocale, {
2522
...options,
2623
timeZone: desiredTimeZone,
@@ -41,37 +38,37 @@ describe("DateFormatter", () => {
4138

4239
it("format date from iso date", () => {
4340
const value = "2024-01-01T00:00:00.000Z";
44-
const text = DateTimeFormat(value);
41+
const text = formatDateTime(value);
4542
expect(text).toBe("Monday, 01/01/2024, 12:00:00 am");
4643
});
4744

4845
it("format date from unix timestamp number", () => {
4946
const value = 1762476112;
50-
const text = DateTimeFormat(value);
47+
const text = formatDateTime(value);
5148
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
5249
});
5350

5451
it("format date from unix timestamp string", () => {
5552
const value = "1762476112";
56-
const text = DateTimeFormat(value);
53+
const text = formatDateTime(value);
5754
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
5855
});
5956

6057
it("catch bad format from string", () => {
6158
const value = "this is not a good date";
62-
const text = DateTimeFormat(value);
59+
const text = formatDateTime(value);
6360
expect(text).toBe("this is not a good date");
6461
});
6562

6663
it("catch bad format from number", () => {
6764
const value = -100;
68-
const text = DateTimeFormat(value);
65+
const text = formatDateTime(value);
6966
expect(text).toBe("-100");
7067
});
7168

7269
it("catch bad format from number as string", () => {
7370
const value = "-100";
74-
const text = DateTimeFormat(value);
71+
const text = formatDateTime(value);
7572
expect(text).toBe("-100");
7673
});
7774
});

frontend/src/locale/DateTimeFormat.ts renamed to frontend/src/locale/Utils.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ const isUnixTimestamp = (value: unknown): boolean => {
1111
return false;
1212
};
1313

14-
const DateTimeFormat = (value: string | number): string => {
15-
if (typeof value !== "number" && typeof value !== "string") return `${value}`;
14+
const parseDate = (value: string | number): Date | null => {
15+
if (typeof value !== "number" && typeof value !== "string") return null;
16+
try {
17+
return isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`);
18+
} catch {
19+
return null;
20+
}
21+
};
1622

23+
const formatDateTime = (value: string | number): string => {
24+
const d = parseDate(value);
25+
if (!d) return `${value}`;
1726
try {
18-
const d = isUnixTimestamp(value)
19-
? fromUnixTime(+value)
20-
: parseISO(`${value}`);
2127
return intlFormat(d, {
2228
weekday: "long",
2329
year: "numeric",
@@ -33,4 +39,4 @@ const DateTimeFormat = (value: string | number): string => {
3339
}
3440
};
3541

36-
export { DateTimeFormat };
42+
export { formatDateTime, parseDate, isUnixTimestamp };

frontend/src/locale/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from "./DateTimeFormat";
21
export * from "./IntlProvider";
2+
export * from "./Utils";

0 commit comments

Comments
 (0)