فهرست منبع

Fixes #4844 with more defensive date parsing

Jamie Curnow 1 ماه پیش
والد
کامیت
3c252db46f

+ 2 - 2
frontend/src/components/Form/AccessField.tsx

@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
 import Select, { type ActionMeta, components, type OptionProps } from "react-select";
 import type { AccessList } from "src/api/backend";
 import { useAccessLists } from "src/hooks";
-import { DateTimeFormat, intl, T } from "src/locale";
+import { formatDateTime, intl, T } from "src/locale";
 
 interface AccessOption {
 	readonly value: number;
@@ -48,7 +48,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
 				{
 					users: item?.items?.length,
 					rules: item?.clients?.length,
-					date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A",
+					date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A",
 				},
 			),
 			icon: <IconLock size={14} className="text-lime" />,

+ 2 - 2
frontend/src/components/Form/SSLCertificateField.tsx

@@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik";
 import Select, { type ActionMeta, components, type OptionProps } from "react-select";
 import type { Certificate } from "src/api/backend";
 import { useCertificates } from "src/hooks";
-import { DateTimeFormat, intl, T } from "src/locale";
+import { formatDateTime, intl, T } from "src/locale";
 
 interface CertOption {
 	readonly value: number | "new";
@@ -75,7 +75,7 @@ export function SSLCertificateField({
 		data?.map((cert: Certificate) => ({
 			value: cert.id,
 			label: cert.niceName,
-			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" })}`,
+			subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider}  ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`,
 			icon: <IconShield size={14} className="text-pink" />,
 		})) || [];
 

+ 6 - 5
frontend/src/components/Table/Formatter/DateFormatter.tsx

@@ -1,6 +1,6 @@
 import cn from "classnames";
-import { differenceInDays, isPast, parseISO } from "date-fns";
-import { DateTimeFormat } from "src/locale";
+import { differenceInDays, isPast } from "date-fns";
+import { formatDateTime, parseDate } from "src/locale";
 
 interface Props {
 	value: string;
@@ -8,11 +8,12 @@ interface Props {
 	highlistNearlyExpired?: boolean;
 }
 export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
-	const dateIsPast = isPast(parseISO(value));
-	const days = differenceInDays(parseISO(value), new Date());
+	const d = parseDate(value);
+	const dateIsPast = d ? isPast(d) : false;
+	const days = d ? differenceInDays(d, new Date()) : 0;
 	const cl = cn({
 		"text-danger": highlightPast && dateIsPast,
 		"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
 	});
-	return <span className={cl}>{DateTimeFormat(value)}</span>;
+	return <span className={cl}>{formatDateTime(value)}</span>;
 }

+ 2 - 2
frontend/src/components/Table/Formatter/DomainsFormatter.tsx

@@ -1,6 +1,6 @@
 import cn from "classnames";
 import type { ReactNode } from "react";
-import { DateTimeFormat, T } from "src/locale";
+import { formatDateTime, T } from "src/locale";
 
 interface Props {
 	domains: string[];
@@ -53,7 +53,7 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
 			<div className="font-weight-medium">{...elms}</div>
 			{createdOn ? (
 				<div className="text-secondary mt-1">
-					<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
+					<T id="created-on" data={{ date: formatDateTime(createdOn) }} />
 				</div>
 			) : null}
 		</div>

+ 2 - 2
frontend/src/components/Table/Formatter/EventFormatter.tsx

@@ -1,7 +1,7 @@
 import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
 import cn from "classnames";
 import type { AuditLog } from "src/api/backend";
-import { DateTimeFormat, T } from "src/locale";
+import { formatDateTime, T } from "src/locale";
 
 const getEventValue = (event: AuditLog) => {
 	switch (event.objectType) {
@@ -73,7 +73,7 @@ export function EventFormatter({ row }: Props) {
 				<T id={`object.event.${row.action}`} tData={{ object: row.objectType }} />
 				&nbsp; &mdash; <span className="badge">{getEventValue(row)}</span>
 			</div>
-			<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div>
+			<div className="text-secondary mt-1">{formatDateTime(row.createdOn)}</div>
 		</div>
 	);
 }

+ 2 - 2
frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx

@@ -1,4 +1,4 @@
-import { DateTimeFormat, T } from "src/locale";
+import { formatDateTime, T } from "src/locale";
 
 interface Props {
 	value: string;
@@ -13,7 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
 			</div>
 			{createdOn ? (
 				<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
-					<T id={disabled ? "disabled" : "created-on"} data={{ date: DateTimeFormat(createdOn) }} />
+					<T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn) }} />
 				</div>
 			) : null}
 		</div>

+ 8 - 11
frontend/src/components/Table/Formatter/DateFormatter.test.tsx → frontend/src/locale/Utils.test.tsx

@@ -1,4 +1,4 @@
-import { DateTimeFormat } from "src/locale";
+import { formatDateTime } from "src/locale";
 import { afterAll, beforeAll, describe, expect, it } from "vitest";
 
 describe("DateFormatter", () => {
@@ -17,10 +17,7 @@ describe("DateFormatter", () => {
 
 		// Mock Intl.DateTimeFormat so formatting is stable regardless of host
 		const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {
-			constructor(
-				_locales?: string | string[],
-				options?: Intl.DateTimeFormatOptions,
-			) {
+			constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) {
 				super(desiredLocale, {
 					...options,
 					timeZone: desiredTimeZone,
@@ -41,37 +38,37 @@ describe("DateFormatter", () => {
 
 	it("format date from iso date", () => {
 		const value = "2024-01-01T00:00:00.000Z";
-		const text = DateTimeFormat(value);
+		const text = formatDateTime(value);
 		expect(text).toBe("Monday, 01/01/2024, 12:00:00 am");
 	});
 
 	it("format date from unix timestamp number", () => {
 		const value = 1762476112;
-		const text = DateTimeFormat(value);
+		const text = formatDateTime(value);
 		expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
 	});
 
 	it("format date from unix timestamp string", () => {
 		const value = "1762476112";
-		const text = DateTimeFormat(value);
+		const text = formatDateTime(value);
 		expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
 	});
 
 	it("catch bad format from string", () => {
 		const value = "this is not a good date";
-		const text = DateTimeFormat(value);
+		const text = formatDateTime(value);
 		expect(text).toBe("this is not a good date");
 	});
 
 	it("catch bad format from number", () => {
 		const value = -100;
-		const text = DateTimeFormat(value);
+		const text = formatDateTime(value);
 		expect(text).toBe("-100");
 	});
 
 	it("catch bad format from number as string", () => {
 		const value = "-100";
-		const text = DateTimeFormat(value);
+		const text = formatDateTime(value);
 		expect(text).toBe("-100");
 	});
 });

+ 12 - 6
frontend/src/locale/DateTimeFormat.ts → frontend/src/locale/Utils.ts

@@ -11,13 +11,19 @@ const isUnixTimestamp = (value: unknown): boolean => {
 	return false;
 };
 
-const DateTimeFormat = (value: string | number): string => {
-	if (typeof value !== "number" && typeof value !== "string") return `${value}`;
+const parseDate = (value: string | number): Date | null => {
+	if (typeof value !== "number" && typeof value !== "string") return null;
+	try {
+		return isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`);
+	} catch {
+		return null;
+	}
+};
 
+const formatDateTime = (value: string | number): string => {
+	const d = parseDate(value);
+	if (!d) return `${value}`;
 	try {
-		const d = isUnixTimestamp(value)
-			? fromUnixTime(+value)
-			: parseISO(`${value}`);
 		return intlFormat(d, {
 			weekday: "long",
 			year: "numeric",
@@ -33,4 +39,4 @@ const DateTimeFormat = (value: string | number): string => {
 	}
 };
 
-export { DateTimeFormat };
+export { formatDateTime, parseDate, isUnixTimestamp };

+ 1 - 1
frontend/src/locale/index.ts

@@ -1,2 +1,2 @@
-export * from "./DateTimeFormat";
 export * from "./IntlProvider";
+export * from "./Utils";