diff --git a/package.json b/package.json
index 3cf0ffb6ff..78288ba104 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"classnames": "2.5.1",
"clone-deep": "4.0.1",
"date-fns": "2.30.0",
+ "date-fns-tz": "2.0.1",
"fast-deep-equal": "3.1.3",
"formik": "2.4.5",
"history": "5.3.0",
diff --git a/src/app/base/components/NotificationGroup/Notification/Notification.tsx b/src/app/base/components/NotificationGroup/Notification/Notification.tsx
index 46bcb5fcb9..5105aeee26 100644
--- a/src/app/base/components/NotificationGroup/Notification/Notification.tsx
+++ b/src/app/base/components/NotificationGroup/Notification/Notification.tsx
@@ -13,7 +13,7 @@ import {
isUpgradeNotification,
} from "@/app/store/notification/utils";
import type { RootState } from "@/app/store/root/types";
-import { getUtcTimestamp } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
type Props = {
className?: string | null;
@@ -32,7 +32,7 @@ const NotificationGroupNotification = ({
const notification = useSelector((state: RootState) =>
notificationSelectors.getById(state, id)
);
- const createdTimestamp = getUtcTimestamp(notification?.created);
+ const createdTimestamp = formatUtcDatetime(notification?.created);
if (!notification) {
return null;
}
diff --git a/src/app/base/components/StatusBar/StatusBar.test.tsx b/src/app/base/components/StatusBar/StatusBar.test.tsx
index 4a7b8a7b58..0a12194c1e 100644
--- a/src/app/base/components/StatusBar/StatusBar.test.tsx
+++ b/src/app/base/components/StatusBar/StatusBar.test.tsx
@@ -164,8 +164,8 @@ it("displays correct text for machines with hardware sync enabled and no last_sy
status: NodeStatus.DEPLOYED,
system_id: "abc123",
enable_hw_sync: true,
- last_sync: factory.timestamp("Thu, 31 Dec. 2020 22:00:00"),
- next_sync: factory.timestamp("Thu, 31 Dec. 2020 23:01:00"),
+ last_sync: factory.timestamp(""),
+ next_sync: factory.timestamp(""),
}),
];
@@ -198,7 +198,7 @@ it("displays last image sync timestamp for a rack or region+rack controller", ()
renderWithMockStore(, { state });
expect(screen.getByTestId("status-bar-status")).toHaveTextContent(
- `Last image sync: ${controller.last_image_sync}`
+ `Last image sync: Thu, 02 Jun. 2022 00:48:41 (UTC)`
);
});
diff --git a/src/app/base/components/StatusBar/StatusBar.tsx b/src/app/base/components/StatusBar/StatusBar.tsx
index 3155c4af88..33e5184b9b 100644
--- a/src/app/base/components/StatusBar/StatusBar.tsx
+++ b/src/app/base/components/StatusBar/StatusBar.tsx
@@ -18,9 +18,9 @@ import {
isDeployedWithHardwareSync,
isMachineDetails,
} from "@/app/store/machine/utils";
-import type { UtcTimestamp } from "@/app/store/types/model";
+import type { UtcDatetime } from "@/app/store/types/model";
import { NodeStatus } from "@/app/store/types/node";
-import { getUtcTimestamp, getTimeDistanceString } from "@/app/utils/time";
+import { formatUtcDatetime, getTimeDistanceString } from "@/app/utils/time";
const getLastCommissionedString = (machine: MachineDetails) => {
if (machine.status === NodeStatus.COMMISSIONING) {
@@ -38,7 +38,7 @@ const getLastCommissionedString = (machine: MachineDetails) => {
}
};
-const getSyncStatusString = (syncStatus: UtcTimestamp) => {
+const getSyncStatusString = (syncStatus: UtcDatetime) => {
if (syncStatus === "") {
return "Never";
}
@@ -88,7 +88,7 @@ export const StatusBar = (): JSX.Element | null => {
isControllerDetails(activeController) &&
(isRack(activeController) || isRegionAndRack(activeController))
) {
- status = `Last image sync: ${getUtcTimestamp(
+ status = `Last image sync: ${formatUtcDatetime(
activeController.last_image_sync
)}`;
}
diff --git a/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx b/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx
index 77b283f103..446afffb47 100644
--- a/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx
+++ b/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx
@@ -19,7 +19,7 @@ import type { ScriptResult } from "@/app/store/scriptresult/types";
import { ScriptResultType } from "@/app/store/scriptresult/types";
import { canBeSuppressed } from "@/app/store/scriptresult/utils";
import { nodeIsMachine } from "@/app/store/utils";
-import { getUtcTimestamp } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
export enum ScriptResultAction {
VIEW_METRICS = "viewMetrics",
@@ -132,7 +132,7 @@ const NodeTestsTable = ({ node, scriptResults }: Props): JSX.Element => {
},
{
className: "date-col",
- content: getUtcTimestamp(result.updated),
+ content: formatUtcDatetime(result.updated),
},
{
className: "runtime-col",
diff --git a/src/app/networkDiscovery/views/DiscoveriesList/DiscoveriesList.tsx b/src/app/networkDiscovery/views/DiscoveriesList/DiscoveriesList.tsx
index 3da80adfd5..8f667773ef 100644
--- a/src/app/networkDiscovery/views/DiscoveriesList/DiscoveriesList.tsx
+++ b/src/app/networkDiscovery/views/DiscoveriesList/DiscoveriesList.tsx
@@ -25,6 +25,7 @@ import type { Discovery } from "@/app/store/discovery/types";
import { DiscoveryMeta } from "@/app/store/discovery/types";
import type { RootState } from "@/app/store/root/types";
import { generateEmptyStateMsg, getTableStatus } from "@/app/utils";
+import { formatUtcDatetime } from "@/app/utils/time";
export enum Labels {
DiscoveriesList = "Discoveries list",
@@ -78,7 +79,11 @@ const generateRows = (
content: discovery.observer_hostname,
},
{
- content:
{discovery.last_seen}
,
+ content: (
+
+ {formatUtcDatetime(discovery.last_seen)}
+
+ ),
},
{
content: (
diff --git a/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx b/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx
index 69c379a7e7..5de926aa5e 100644
--- a/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx
+++ b/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx
@@ -33,7 +33,7 @@ import type { RootState } from "@/app/store/root/types";
import { subnetActions } from "@/app/store/subnet";
import subnetSelectors from "@/app/store/subnet/selectors";
import type { Subnet } from "@/app/store/subnet/types";
-import { formatUtcTimestamp } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
const getTargetName = (
controllers: Controller[],
@@ -80,7 +80,7 @@ const generateRows = (
const expanded = expandedId === dhcpsnippet.id;
// Dates are in the format: Thu, 15 Aug. 2019 06:21:39.
const updated = dhcpsnippet.updated
- ? formatUtcTimestamp(dhcpsnippet.updated)
+ ? formatUtcDatetime(dhcpsnippet.updated)
: "Never";
const enabled = dhcpsnippet.enabled ? "Yes" : "No";
const showDelete = expandedType === "delete";
diff --git a/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx b/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx
index 8bb51eb836..9de2e14d0b 100644
--- a/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx
+++ b/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx
@@ -315,7 +315,7 @@ describe("ScriptsList", () => {
);
expect(
within(screen.getByRole("row", { name: "test name 33" })).getByText(
- "2020-12-31 22:59"
+ "Thu, 31 Dec. 2020 22:59:00 (UTC)"
)
).toBeInTheDocument();
});
@@ -326,7 +326,7 @@ describe("ScriptsList", () => {
loaded: true,
items: [
factory.script({
- created: factory.timestamp(""),
+ created: () => factory.timestamp(""),
script_type: ScriptType.TESTING,
}),
],
diff --git a/src/app/settings/views/Scripts/ScriptsList/ScriptsList.tsx b/src/app/settings/views/Scripts/ScriptsList/ScriptsList.tsx
index 3ac9498df3..05c3fee0ba 100644
--- a/src/app/settings/views/Scripts/ScriptsList/ScriptsList.tsx
+++ b/src/app/settings/views/Scripts/ScriptsList/ScriptsList.tsx
@@ -1,7 +1,6 @@
import { useEffect, useState } from "react";
import { ContentSection } from "@canonical/maas-react-components";
-import { format } from "date-fns";
import { useDispatch, useSelector } from "react-redux";
import type { Dispatch } from "redux";
@@ -17,7 +16,7 @@ import { scriptActions } from "@/app/store/script";
import scriptSelectors from "@/app/store/script/selectors";
import type { Script } from "@/app/store/script/types";
import { generateEmptyStateMsg, getTableStatus } from "@/app/utils";
-import { parseUtcDatetime } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
export enum Labels {
Actions = "Table actions",
@@ -49,7 +48,7 @@ const generateRows = (
// history timestamps are in the format: Mon, 02 Sep 2019 02:02:39 -0000
let uploadedOn: string;
try {
- uploadedOn = format(parseUtcDatetime(script.created), "yyyy-LL-dd H:mm");
+ uploadedOn = formatUtcDatetime(script.created);
} catch (error) {
uploadedOn = "Never";
}
diff --git a/src/app/settings/views/Users/UsersList/UsersList.tsx b/src/app/settings/views/Users/UsersList/UsersList.tsx
index 66d235a145..3247dfdfbd 100644
--- a/src/app/settings/views/Users/UsersList/UsersList.tsx
+++ b/src/app/settings/views/Users/UsersList/UsersList.tsx
@@ -22,7 +22,7 @@ import { userActions } from "@/app/store/user";
import userSelectors from "@/app/store/user/selectors";
import type { User } from "@/app/store/user/types";
import { isComparable } from "@/app/utils";
-import { formatUtcTimestamp } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
type SortKey = keyof User;
@@ -35,7 +35,7 @@ const generateUserRows = (
const isAuthUser = user.id === authUser?.id;
// Dates are in the format: Thu, 15 Aug. 2019 06:21:39.
const last_login = user.last_login
- ? formatUtcTimestamp(user.last_login)
+ ? formatUtcDatetime(user.last_login)
: "Never";
const fullName = user.last_name;
return {
diff --git a/src/app/store/bootresource/types/base.ts b/src/app/store/bootresource/types/base.ts
index 08402eeb72..6a73e9b0e7 100644
--- a/src/app/store/bootresource/types/base.ts
+++ b/src/app/store/bootresource/types/base.ts
@@ -1,6 +1,6 @@
import type { BootResourceSourceType, BootResourceType } from "./enum";
-import type { Model, UtcTimestamp } from "@/app/store/types/model";
+import type { Model, UtcDatetime } from "@/app/store/types/model";
export type BaseImageFields = {
checked: boolean;
@@ -15,8 +15,8 @@ export type BootResource = Model & {
complete: boolean;
downloading: boolean;
icon: "in-progress" | "queued" | "succeeded" | "waiting";
- lastUpdate: UtcTimestamp;
- lastDeployed: UtcTimestamp;
+ lastUpdate: UtcDatetime;
+ lastDeployed: UtcDatetime;
machineCount: number;
name: string;
numberOfNodes: number;
diff --git a/src/app/store/controller/types/base.ts b/src/app/store/controller/types/base.ts
index e976808868..0a65a5a690 100644
--- a/src/app/store/controller/types/base.ts
+++ b/src/app/store/controller/types/base.ts
@@ -11,7 +11,7 @@ import type { PowerState, StorageLayout } from "@/app/store/types/enum";
import type {
ModelRef,
TimestampFields,
- UtcTimestamp,
+ UtcDatetime,
} from "@/app/store/types/model";
import type {
NodeActions,
@@ -69,7 +69,7 @@ export type ControllerActions =
export type BaseController = BaseNode & {
actions: ControllerActions[];
- last_image_sync: UtcTimestamp;
+ last_image_sync: UtcDatetime;
link_type: NodeLinkType.CONTROLLER;
node_type_display:
| NodeTypeDisplay.RACK_CONTROLLER
diff --git a/src/app/store/discovery/selectors.test.ts b/src/app/store/discovery/selectors.test.ts
index b4ba5d1600..ceabed0bbc 100644
--- a/src/app/store/discovery/selectors.test.ts
+++ b/src/app/store/discovery/selectors.test.ts
@@ -62,7 +62,7 @@ describe("discovery selectors", () => {
mac_organization: "Acme Inc.",
ip: "0.0.0.0",
observer_hostname: "alpha",
- last_seen: "Mon, 19 Oct. 2020 01:15:57",
+ last_seen: factory.timestamp("Mon, 19 Oct. 2020 01:15:57"),
}),
factory.discovery({
hostname: "bar",
@@ -70,7 +70,7 @@ describe("discovery selectors", () => {
mac_organization: "Foodies Inc.",
ip: "1.1.1.1",
observer_hostname: "bravo",
- last_seen: "Sat, 17 Oct. 2020 01:15:57",
+ last_seen: factory.timestamp("Sat, 17 Oct. 2020 01:15:57"),
}),
factory.discovery({
hostname: "foobar",
@@ -78,7 +78,7 @@ describe("discovery selectors", () => {
mac_organization: "Roxxon",
ip: "2.2.2.2",
observer_hostname: "foot",
- last_seen: "Mon, 19 Oct. 2020 01:15:57",
+ last_seen: factory.timestamp("Mon, 19 Oct. 2020 01:15:57"),
}),
factory.discovery({
hostname: "fizz",
@@ -86,7 +86,7 @@ describe("discovery selectors", () => {
mac_organization: "Pacific Couriers",
ip: "3.3.3.3",
observer_hostname: "alpha",
- last_seen: "Mon, 19 Oct. 2020 01:15:57",
+ last_seen: factory.timestamp("Mon, 19 Oct. 2020 01:15:57"),
}),
],
}),
diff --git a/src/app/store/discovery/types/base.ts b/src/app/store/discovery/types/base.ts
index b4ed8ba645..a600fb43f2 100644
--- a/src/app/store/discovery/types/base.ts
+++ b/src/app/store/discovery/types/base.ts
@@ -1,5 +1,5 @@
import type { APIError } from "@/app/base/types";
-import type { Model } from "@/app/store/types/model";
+import type { Model, UtcDatetime } from "@/app/store/types/model";
import type { GenericState } from "@/app/store/types/state";
export type Discovery = Model & {
@@ -10,7 +10,7 @@ export type Discovery = Model & {
hostname: string | null;
ip: string | null;
is_external_dhcp: boolean | null;
- last_seen: string;
+ last_seen: UtcDatetime;
mac_address: string | null;
mac_organization: string;
mdns: number | null;
diff --git a/src/app/store/machine/types/base.ts b/src/app/store/machine/types/base.ts
index e265e42406..b53e6b4401 100644
--- a/src/app/store/machine/types/base.ts
+++ b/src/app/store/machine/types/base.ts
@@ -8,7 +8,7 @@ import type { PowerState, StorageLayout } from "@/app/store/types/enum";
import type {
ModelRef,
TimestampFields,
- UtcTimestamp,
+ UtcDatetime,
} from "@/app/store/types/model";
import type {
BaseNode,
@@ -73,7 +73,7 @@ export type MachineDetails = BaseMachine &
bmc: number;
boot_disk: Disk | null;
certificate?: CertificateMetadata;
- commissioning_start_time: UtcTimestamp;
+ commissioning_start_time: UtcDatetime;
commissioning_status: TestStatus;
cpu_speed: BaseNode["cpu_speed"];
cpu_test_status: TestStatus;
@@ -126,8 +126,8 @@ type HardwareSyncFields =
}
| {
enable_hw_sync: true;
- last_sync: UtcTimestamp;
- next_sync: UtcTimestamp;
+ last_sync: UtcDatetime;
+ next_sync: UtcDatetime;
is_sync_healthy: boolean;
sync_interval: Seconds;
};
diff --git a/src/app/store/scriptresult/types/base.ts b/src/app/store/scriptresult/types/base.ts
index b81a85567b..54afbdcb48 100644
--- a/src/app/store/scriptresult/types/base.ts
+++ b/src/app/store/scriptresult/types/base.ts
@@ -9,7 +9,7 @@ import type {
import type { HardwareType } from "@/app/base/enum";
import type { APIError } from "@/app/base/types";
-import type { Model, UtcTimestamp } from "@/app/store/types/model";
+import type { Model, UtcDatetime } from "@/app/store/types/model";
import type { NetworkInterface } from "@/app/store/types/node";
import type { GenericState } from "@/app/store/types/state";
@@ -29,7 +29,7 @@ export type PartialScriptResult = Model & {
status: ScriptResultStatus;
status_name: string;
suppressed: boolean;
- updated?: UtcTimestamp;
+ updated?: UtcDatetime;
};
export type ScriptResult = PartialScriptResult & {
@@ -76,7 +76,7 @@ export type ScriptResult = PartialScriptResult & {
results: ScriptResultResult[];
script?: number;
script_version?: number | null;
- started?: UtcTimestamp;
+ started?: UtcDatetime;
tags: string;
};
diff --git a/src/app/store/types/model.ts b/src/app/store/types/model.ts
index 8d48cdd8fe..e3630552a3 100644
--- a/src/app/store/types/model.ts
+++ b/src/app/store/types/model.ts
@@ -3,10 +3,11 @@ export type Model = {
};
// Expected format: "Thu, 15 Aug. 2019 06:21:39" or ""
-export type UtcTimestamp = string & { readonly __brand: unique symbol };
+export type UtcDatetime = string & { readonly __brand: unique symbol };
+export type UtcDatetimeDisplay = `${string} (UTC)` | "Never";
export type TimestampFields = {
- created: UtcTimestamp;
- updated: UtcTimestamp;
+ created: UtcDatetime;
+ updated: UtcDatetime;
};
export type TimestampedModel = Model & TimestampFields;
diff --git a/src/app/store/user/types/base.ts b/src/app/store/user/types/base.ts
index 8891dbfef2..5b9f9f9e31 100644
--- a/src/app/store/user/types/base.ts
+++ b/src/app/store/user/types/base.ts
@@ -1,5 +1,5 @@
import type { APIError } from "@/app/base/types";
-import type { Model, UtcTimestamp } from "@/app/store/types/model";
+import type { Model, UtcDatetime } from "@/app/store/types/model";
import type { GenericState } from "@/app/store/types/state";
export type User = Model & {
@@ -9,7 +9,7 @@ export type User = Model & {
is_local: boolean;
is_superuser: boolean;
last_name: string;
- last_login: UtcTimestamp;
+ last_login: UtcDatetime;
machines_count: number;
sshkeys_count: number;
username: string;
diff --git a/src/app/tags/views/TagList/TagTable/TagTable.tsx b/src/app/tags/views/TagList/TagTable/TagTable.tsx
index 4ff736f2fa..539eab3775 100644
--- a/src/app/tags/views/TagList/TagTable/TagTable.tsx
+++ b/src/app/tags/views/TagList/TagTable/TagTable.tsx
@@ -24,7 +24,7 @@ import type { Tag } from "@/app/store/tag/types";
import { TagMeta } from "@/app/store/tag/types";
import AppliedTo from "@/app/tags/components/AppliedTo";
import { isComparable } from "@/app/utils";
-import { getUtcTimestamp } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
type Props = PropsWithSpread<
{
@@ -76,7 +76,7 @@ const generateRows = (
},
{
"aria-label": Label.Updated,
- content: getUtcTimestamp(tag.updated),
+ content: formatUtcDatetime(tag.updated),
},
{
"aria-label": Label.Auto,
diff --git a/src/app/tags/views/TagUpdate/TagUpdateFormFields/TagUpdateFormFields.tsx b/src/app/tags/views/TagUpdate/TagUpdateFormFields/TagUpdateFormFields.tsx
index 17e06ab835..2144082bbb 100644
--- a/src/app/tags/views/TagUpdate/TagUpdateFormFields/TagUpdateFormFields.tsx
+++ b/src/app/tags/views/TagUpdate/TagUpdateFormFields/TagUpdateFormFields.tsx
@@ -12,7 +12,7 @@ import AppliedTo from "@/app/tags/components/AppliedTo";
import DefinitionField from "@/app/tags/components/DefinitionField";
import KernelOptionsField from "@/app/tags/components/KernelOptionsField";
import { Label } from "@/app/tags/views/TagDetails";
-import { getUtcTimestamp } from "@/app/utils/time";
+import { formatUtcDatetime } from "@/app/utils/time";
type Props = {
id: Tag[TagMeta.PK];
@@ -41,7 +41,7 @@ export const TagUpdateFormFields = ({ id }: Props): JSX.Element | null => {
diff --git a/src/app/utils/time.test.ts b/src/app/utils/time.test.ts
index 62944bbd32..510c5d4bae 100644
--- a/src/app/utils/time.test.ts
+++ b/src/app/utils/time.test.ts
@@ -2,59 +2,74 @@ import MockDate from "mockdate";
import timezoneMock from "timezone-mock";
import {
- formatUtcTimestamp,
+ formatUtcDatetime,
getTimeDistanceString,
- getUtcTimestamp,
+ parseUtcDatetime,
} from "./time";
-import type { UtcTimestamp } from "@/app/store/types/model";
-import * as factory from "@/testing/factories";
+import type { UtcDatetime } from "@/app/store/types/model";
beforeEach(() => {
MockDate.set("Fri, 18 Nov. 2022 01:01:00");
+ timezoneMock.register("Etc/GMT+5");
});
afterEach(() => {
MockDate.reset();
+ timezoneMock.unregister();
});
describe("getTimeDistanceString", () => {
it("returns time distance for UTC TimeString in the past", () => {
expect(
- getTimeDistanceString("Fri, 18 Nov. 2022 01:00:50" as UtcTimestamp)
+ getTimeDistanceString("Fri, 18 Nov. 2022 01:00:50" as UtcDatetime)
).toEqual("less than a minute ago");
});
it("returns time distance for UTC TimeString in the future", () => {
expect(
- getTimeDistanceString("Fri, 18 Nov. 2022 01:01:10" as UtcTimestamp)
+ getTimeDistanceString("Fri, 18 Nov. 2022 01:01:10" as UtcDatetime)
).toEqual("in less than a minute");
});
});
-describe("formatUtcTimestamp", () => {
+describe("formatUtcDatetime", () => {
it("returns UTC date time in a correct format", () => {
+ timezoneMock.register("Etc/GMT+0");
expect(
- formatUtcTimestamp("Fri, 18 Nov. 2022 01:00:50" as UtcTimestamp)
- ).toEqual("Fri, 18 Nov. 2022 01:00:50");
+ formatUtcDatetime("Fri, 18 Nov. 2022 01:00:50" as UtcDatetime)
+ ).toEqual("Fri, 18 Nov. 2022 01:00:50 (UTC)");
});
- it("returns UTC date time in local time", () => {
+ it("returns UTC date time in UTC regardless of timezone", () => {
timezoneMock.register("Etc/GMT-1");
expect(
- formatUtcTimestamp("Fri, 18 Nov. 2022 03:00:00" as UtcTimestamp)
- ).toEqual("Fri, 18 Nov. 2022 04:00:00");
+ formatUtcDatetime("Fri, 18 Nov. 2022 03:00:00" as UtcDatetime)
+ ).toEqual("Fri, 18 Nov. 2022 03:00:00 (UTC)");
+ });
+ it("returns Never if no time is provided", () => {
+ const inputTimeString = "" as UtcDatetime;
+ const expectedOutput = "Never";
+ expect(formatUtcDatetime(inputTimeString)).toEqual(expectedOutput);
});
-});
-describe("getUtcTimestamp", () => {
it("appends (UTC) to the given time string", () => {
- const inputTimeString = "Fri, 18 Nov. 2022 01:00:50" as UtcTimestamp;
+ const inputTimeString = "Fri, 18 Nov. 2022 01:00:50" as UtcDatetime;
const expectedOutput = "Fri, 18 Nov. 2022 01:00:50 (UTC)";
- expect(getUtcTimestamp(inputTimeString)).toEqual(expectedOutput);
+ expect(formatUtcDatetime(inputTimeString)).toEqual(expectedOutput);
+ });
+});
+
+describe("parseUtcDatetime", () => {
+ it("parses UTC time string into Date object correctly", () => {
+ const utcTimeString = "Fri, 18 Nov. 2022 01:00:50" as UtcDatetime;
+ const expectedDate = new Date(Date.UTC(2022, 10, 18, 1, 0, 50)); // Fri, 18 Nov. 2022 01:00:50
+ const result = parseUtcDatetime(utcTimeString);
+ expect(result).toEqual(expectedDate);
});
- it("works with different date formats", () => {
- const inputTimeString = factory.timestamp("2022-11-18T01:00:50Z");
- const expectedOutput = "2022-11-18T01:00:50Z (UTC)";
- expect(getUtcTimestamp(inputTimeString)).toEqual(expectedOutput);
+ it("handles leap years correctly", () => {
+ const utcTimeString = "Mon, 29 Feb. 2016 12:00:00" as UtcDatetime;
+ const expectedDate = new Date(Date.UTC(2016, 1, 29, 12, 0, 0)); // Mon, 29 Feb. 2016 12:00:00
+ const result = parseUtcDatetime(utcTimeString);
+ expect(result).toEqual(expectedDate);
});
});
diff --git a/src/app/utils/time.ts b/src/app/utils/time.ts
index e342ecf822..4706341a9d 100644
--- a/src/app/utils/time.ts
+++ b/src/app/utils/time.ts
@@ -1,40 +1,40 @@
-import { format, formatDistance, parse } from "date-fns";
+import { formatDistance, parse } from "date-fns";
+import { formatInTimeZone } from "date-fns-tz";
-import type { UtcTimestamp } from "@/app/store/types/model";
+import type { UtcDatetime, UtcDatetimeDisplay } from "@/app/store/types/model";
-const DATETIME_FORMAT = "E, dd LLL. yyyy HH:mm:ss";
-const UTC_DATETIME_FORMAT = `${DATETIME_FORMAT} x`;
+const DATETIME_DISPLAY_FORMAT = "E, dd LLL. yyyy HH:mm:ss";
-export const parseUtcDatetime = (utcTimeString: UtcTimestamp): Date =>
+export const parseUtcDatetime = (utcTimeString: UtcDatetime): Date =>
parse(
`${utcTimeString} +00`, // let parse fn know it's UTC
- UTC_DATETIME_FORMAT,
+ `${DATETIME_DISPLAY_FORMAT} x`,
new Date()
);
-export const getTimeDistanceString = (utcTimeString: UtcTimestamp): string =>
+export const getTimeDistanceString = (utcTimeString: UtcDatetime) =>
formatDistance(parseUtcDatetime(utcTimeString), new Date(), {
addSuffix: true,
});
/**
- * Appends "(UTC)" to a given string to indicate the time zone explicitly.
- * @param utcTimeString - time string in UTC
- * @returns Time string with appended "(UTC)"
+ * @param utcTimeString - time string in UTC_DATETIME_FORMAT
+ * @returns time string adjusted for local time zone in DATETIME_FORMAT
*/
-export const getUtcTimestamp = (utcTimeString?: UtcTimestamp): string =>
- utcTimeString ? utcTimeString + " (UTC)" : "";
+export const formatUtcDatetime = (
+ utcTimeString?: UtcDatetime
+): UtcDatetimeDisplay => {
+ if (!utcTimeString) return "Never";
-/**
- * Formats a given UTC time string into a more readable format and appends "(UTC)" to indicate the time zone explicitly.
- * It converts the time string from "E, dd LLL. yyyy HH:mm:ss" format to "yyyy-LL-dd H:mm" format.
- * @param utcTimeString - time string in UTC to be formatted
- * @returns Formatted time string with appended "(UTC)"
- */
-export const formatUtcTimestamp = (utcTimeString?: UtcTimestamp): string =>
- utcTimeString
- ? format(
- parse(utcTimeString, "E, dd LLL. yyyy HH:mm:ss", new Date()),
- "yyyy-LL-dd H:mm"
- ) + " (UTC)"
- : "";
+ try {
+ const utcTime = `${formatInTimeZone(
+ parseUtcDatetime(utcTimeString),
+ "UTC",
+ DATETIME_DISPLAY_FORMAT,
+ { addSuffix: true }
+ )} (UTC)` as const;
+ return utcTime;
+ } catch (error) {
+ return "Never";
+ }
+};
diff --git a/src/testing/factories/discovery.ts b/src/testing/factories/discovery.ts
index 82dba7bd46..73a94f7750 100644
--- a/src/testing/factories/discovery.ts
+++ b/src/testing/factories/discovery.ts
@@ -1,5 +1,6 @@
import { extend, random } from "cooky-cutter";
+import { timestamp } from "./general";
import { model } from "./model";
import type { Discovery } from "@/app/store/discovery/types";
@@ -13,7 +14,7 @@ export const discovery = extend(model, {
hostname: "discovery-hostname",
ip: "192.168.1.1",
is_external_dhcp: false,
- last_seen: "Wed, 08 Jul. 2020 05:35:4",
+ last_seen: () => timestamp("Wed, 08 Jul. 2020 05:35:4"),
mac_address: "00:00:00:00:00:00",
mac_organization: "Business Corp, Inc.",
mdns: 2,
diff --git a/src/testing/factories/general.ts b/src/testing/factories/general.ts
index 5c1acaf7c9..0f6af4551a 100644
--- a/src/testing/factories/general.ts
+++ b/src/testing/factories/general.ts
@@ -29,7 +29,7 @@ import {
PowerFieldScope,
PowerFieldType,
} from "@/app/store/general/types";
-import type { UtcTimestamp } from "@/app/store/types/model";
+import type { UtcDatetime } from "@/app/store/types/model";
import { NodeActions } from "@/app/store/types/node";
export const architecture = define("amd64");
@@ -129,4 +129,6 @@ export const tlsCertificate = define({
export const version = define("test version");
export const timestamp = (timestamp: string) =>
- (timestamp as UtcTimestamp) || ("Wed, 08 Jul. 2020 05:35:4" as UtcTimestamp);
+ timestamp !== undefined
+ ? (timestamp as UtcDatetime)
+ : ("Wed, 08 Jul. 2020 05:35:4" as UtcDatetime);
diff --git a/yarn.lock b/yarn.lock
index 62b91a7db1..2027f6fc43 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6848,6 +6848,11 @@ data-urls@^5.0.0:
whatwg-mimetype "^4.0.0"
whatwg-url "^14.0.0"
+date-fns-tz@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-2.0.1.tgz#0a9b2099031c0d74120b45de9fd23192e48ea495"
+ integrity sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==
+
date-fns@2.30.0, date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
@@ -12461,6 +12466,7 @@ string-natural-compare@^3.0.1:
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
+ name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -12544,6 +12550,7 @@ string_decoder@~1.1.1:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
+ name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13735,6 +13742,7 @@ wordwrap@^1.0.0:
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+ name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==