mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-11-07 19:05:15 +00:00
Fixes #4844 with more defensive date parsing
This commit is contained in:
@@ -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" />,
|
||||
|
||||
@@ -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} — ${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" />,
|
||||
})) || [];
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }} />
|
||||
— <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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,2 +1,2 @@
|
||||
export * from "./DateTimeFormat";
|
||||
export * from "./IntlProvider";
|
||||
export * from "./Utils";
|
||||
|
||||
Reference in New Issue
Block a user