diff --git a/src/app/base/components/DhcpForm/DhcpForm.test.tsx b/src/app/base/components/DhcpForm/DhcpForm.test.tsx index 4463abce2aa..eceb81bc751 100644 --- a/src/app/base/components/DhcpForm/DhcpForm.test.tsx +++ b/src/app/base/components/DhcpForm/DhcpForm.test.tsx @@ -22,17 +22,17 @@ describe("DhcpForm", () => { dhcpsnippet: factory.dhcpSnippetState({ items: [ factory.dhcpSnippet({ - created: "Thu, 15 Aug. 2019 06:21:39", + created: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), id: 1, name: "lease", - updated: "Thu, 15 Aug. 2019 06:21:39", + updated: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), value: "lease 10", }), factory.dhcpSnippet({ - created: "Thu, 15 Aug. 2019 06:21:39", + created: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), id: 2, name: "class", - updated: "Thu, 15 Aug. 2019 06:21:39", + updated: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), }), ], loaded: true, diff --git a/src/app/base/components/DhcpFormFields/DhcpFormFields.test.tsx b/src/app/base/components/DhcpFormFields/DhcpFormFields.test.tsx index 8da85b0c325..c62a11cc134 100644 --- a/src/app/base/components/DhcpFormFields/DhcpFormFields.test.tsx +++ b/src/app/base/components/DhcpFormFields/DhcpFormFields.test.tsx @@ -37,17 +37,17 @@ describe("DhcpFormFields", () => { dhcpsnippet: factory.dhcpSnippetState({ items: [ factory.dhcpSnippet({ - created: "Thu, 15 Aug. 2019 06:21:39", + created: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), id: 1, name: "lease", - updated: "Thu, 15 Aug. 2019 06:21:39", + updated: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), value: "lease 10", }), factory.dhcpSnippet({ - created: "Thu, 15 Aug. 2019 06:21:39", + created: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), id: 2, name: "class", - updated: "Thu, 15 Aug. 2019 06:21:39", + updated: factory.timestamp("Thu, 15 Aug. 2019 06:21:39"), }), ], loaded: true, diff --git a/src/app/base/components/NotificationGroup/Notification/Notification.test.tsx b/src/app/base/components/NotificationGroup/Notification/Notification.test.tsx index 697f2da8fbf..dabc40b0e61 100644 --- a/src/app/base/components/NotificationGroup/Notification/Notification.test.tsx +++ b/src/app/base/components/NotificationGroup/Notification/Notification.test.tsx @@ -101,7 +101,7 @@ describe("NotificationGroupNotification", () => { it("shows the date for upgrade notifications", () => { const notification = factory.notification({ - created: "Tue, 27 Apr. 2021 00:34:39", + created: factory.timestamp("Tue, 27 Apr. 2021 00:34:39"), ident: NotificationIdent.UPGRADE_STATUS, }); const state = factory.rootState({ diff --git a/src/app/base/components/StatusBar/StatusBar.test.tsx b/src/app/base/components/StatusBar/StatusBar.test.tsx index 017a66b6bc1..4a7b8a7b583 100644 --- a/src/app/base/components/StatusBar/StatusBar.test.tsx +++ b/src/app/base/components/StatusBar/StatusBar.test.tsx @@ -52,7 +52,7 @@ it("can show if a machine is currently commissioning", () => { it("can show if a machine has not been commissioned yet", () => { state.machine.items = [ factory.machineDetails({ - commissioning_start_time: "", + commissioning_start_time: factory.timestamp(""), fqdn: "test.maas", system_id: "abc123", }), @@ -67,7 +67,7 @@ it("can show the last time a machine was commissioned", () => { state.machine.items = [ factory.machineDetails({ enable_hw_sync: false, - commissioning_start_time: "Thu, 31 Dec. 2020 22:59:00", + commissioning_start_time: factory.timestamp("Thu, 31 Dec. 2020 22:59:00"), fqdn: "test.maas", status: NodeStatus.DEPLOYED, system_id: "abc123", @@ -85,7 +85,7 @@ it("can handle an incorrectly formatted commissioning timestamp", () => { state.machine.items = [ factory.machineDetails({ enable_hw_sync: false, - commissioning_start_time: "2020-03-01 09:12:43", + commissioning_start_time: factory.timestamp("2020-03-01 09:12:43"), fqdn: "test.maas", status: NodeStatus.DEPLOYED, system_id: "abc123", @@ -102,13 +102,13 @@ it("can handle an incorrectly formatted commissioning timestamp", () => { it("displays Last and Next sync instead of Last commissioned date for deployed machines with hardware sync enabled ", () => { state.machine.items = [ factory.machineDetails({ - commissioning_start_time: "Thu, 31 Dec. 2020 22:59:00", + commissioning_start_time: factory.timestamp("Thu, 31 Dec. 2020 22:59:00"), fqdn: "test.maas", status: NodeStatus.DEPLOYED, system_id: "abc123", enable_hw_sync: true, - last_sync: "Thu, 31 Dec. 2020 22:00:00", - next_sync: "Thu, 31 Dec. 2020 23:01:00", + last_sync: factory.timestamp("Thu, 31 Dec. 2020 22:00:00"), + next_sync: factory.timestamp("Thu, 31 Dec. 2020 23:01:00"), }), ]; @@ -131,13 +131,13 @@ it("displays Last and Next sync instead of Last commissioned date for deployed m it("doesn't display last or next sync for deploying machines with hardware sync enabled", () => { state.machine.items = [ factory.machineDetails({ - commissioning_start_time: "Thu, 31 Dec. 2020 22:59:00", + commissioning_start_time: factory.timestamp("Thu, 31 Dec. 2020 22:59:00"), fqdn: "test.maas", status: NodeStatus.DEPLOYING, system_id: "abc123", enable_hw_sync: true, - last_sync: "Thu, 31 Dec. 2020 22:00:00", - next_sync: "Thu, 31 Dec. 2020 23:01:00", + last_sync: factory.timestamp("Thu, 31 Dec. 2020 22:00:00"), + next_sync: factory.timestamp("Thu, 31 Dec. 2020 23:01:00"), }), ]; @@ -159,13 +159,13 @@ it("doesn't display last or next sync for deploying machines with hardware sync it("displays correct text for machines with hardware sync enabled and no last_sync or next_sync", () => { state.machine.items = [ factory.machineDetails({ - commissioning_start_time: "Thu, 31 Dec. 2020 22:59:00", + commissioning_start_time: factory.timestamp("Thu, 31 Dec. 2020 22:59:00"), fqdn: "test.maas", status: NodeStatus.DEPLOYED, system_id: "abc123", enable_hw_sync: true, - last_sync: "", - next_sync: "", + last_sync: factory.timestamp("Thu, 31 Dec. 2020 22:00:00"), + next_sync: factory.timestamp("Thu, 31 Dec. 2020 23:01:00"), }), ]; @@ -189,7 +189,7 @@ it("displays correct text for machines with hardware sync enabled and no last_sy it("displays last image sync timestamp for a rack or region+rack controller", () => { const controller = factory.controllerDetails({ - last_image_sync: "Thu, 02 Jun. 2022 00:48:41", + last_image_sync: factory.timestamp("Thu, 02 Jun. 2022 00:48:41"), node_type: NodeType.RACK_CONTROLLER, }); state.controller.active = controller.system_id; diff --git a/src/app/base/components/StatusBar/StatusBar.tsx b/src/app/base/components/StatusBar/StatusBar.tsx index 14572d968ff..3155c4af88f 100644 --- a/src/app/base/components/StatusBar/StatusBar.tsx +++ b/src/app/base/components/StatusBar/StatusBar.tsx @@ -18,8 +18,9 @@ import { isDeployedWithHardwareSync, isMachineDetails, } from "@/app/store/machine/utils"; +import type { UtcTimestamp } from "@/app/store/types/model"; import { NodeStatus } from "@/app/store/types/node"; -import { getTimeDistanceString } from "@/app/utils/time"; +import { getUtcTimestamp, getTimeDistanceString } from "@/app/utils/time"; const getLastCommissionedString = (machine: MachineDetails) => { if (machine.status === NodeStatus.COMMISSIONING) { @@ -37,7 +38,7 @@ const getLastCommissionedString = (machine: MachineDetails) => { } }; -const getSyncStatusString = (syncStatus: string) => { +const getSyncStatusString = (syncStatus: UtcTimestamp) => { if (syncStatus === "") { return "Never"; } @@ -87,7 +88,9 @@ export const StatusBar = (): JSX.Element | null => { isControllerDetails(activeController) && (isRack(activeController) || isRegionAndRack(activeController)) ) { - status = `Last image sync: ${activeController.last_image_sync}`; + status = `Last image sync: ${getUtcTimestamp( + activeController.last_image_sync + )}`; } return ( diff --git a/src/app/base/components/node/NodeLogs/EventLogs/EventLogs.test.tsx b/src/app/base/components/node/NodeLogs/EventLogs/EventLogs.test.tsx index d5645502d58..2b53485d9bc 100644 --- a/src/app/base/components/node/NodeLogs/EventLogs/EventLogs.test.tsx +++ b/src/app/base/components/node/NodeLogs/EventLogs/EventLogs.test.tsx @@ -60,7 +60,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } @@ -116,7 +116,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } @@ -156,11 +156,11 @@ describe("EventLogs", () => { it("orders the rows by most recent first", () => { state.event.items = [ factory.eventRecord({ - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), node_id: 1, }), factory.eventRecord({ - created: "Tue, 17 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 17 Mar. 2021 03:04:00"), node_id: 1, }), ]; @@ -221,7 +221,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } @@ -239,7 +239,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } @@ -273,7 +273,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } @@ -292,7 +292,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } @@ -311,7 +311,7 @@ describe("EventLogs", () => { state.event.items.push( factory.eventRecord({ node_id: 1, - created: "Tue, 16 Mar. 2021 03:04:00", + created: factory.timestamp("Tue, 16 Mar. 2021 03:04:00"), }) ); } diff --git a/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx b/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx index 8a82a260e8d..77b283f1035 100644 --- a/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx +++ b/src/app/base/components/node/NodeTestsTable/NodeTestsTable.tsx @@ -19,6 +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"; export enum ScriptResultAction { VIEW_METRICS = "viewMetrics", @@ -131,7 +132,7 @@ const NodeTestsTable = ({ node, scriptResults }: Props): JSX.Element => { }, { className: "date-col", - content: result.updated, + content: getUtcTimestamp(result.updated), }, { className: "runtime-col", diff --git a/src/app/images/components/ImagesTable/ImagesTable.test.tsx b/src/app/images/components/ImagesTable/ImagesTable.test.tsx index 9c7a2b30e5f..7f4bbfe8cc8 100644 --- a/src/app/images/components/ImagesTable/ImagesTable.test.tsx +++ b/src/app/images/components/ImagesTable/ImagesTable.test.tsx @@ -98,7 +98,7 @@ it("renders the correct status for a downloaded image that is not selected", () }); it("renders the time of last update", () => { - const lastUpdate = "Mon, 30 Jan. 2023 15:54:44"; + const lastUpdate = factory.timestamp("Mon, 30 Jan. 2023 15:54:44"); const resource = factory.bootResource({ arch: "amd64", complete: true, @@ -304,7 +304,7 @@ it("disables delete action for images being downloaded", async () => { }); it("displays a correct last deployed time and machine count", () => { - const lastDeployed = "Fri, 18 Nov. 2022 09:55:21"; + const lastDeployed = factory.timestamp("Fri, 18 Nov. 2022 09:55:21"); const resources = [ factory.bootResource({ arch: "amd64", @@ -358,7 +358,7 @@ it("can handle empty string for last deployed time", () => { factory.bootResource({ arch: "amd64", name: "ubuntu/focal", - lastDeployed: "", + lastDeployed: factory.timestamp(""), machineCount: 768, }), ]; @@ -403,20 +403,20 @@ it("can sort by last deployed time", async () => { name: "ubuntu/xenial", arch: "amd64", title: "16.04 LTS", - lastDeployed: "Tue, 16 Nov. 2022 09:55:21", + lastDeployed: factory.timestamp("Tue, 16 Nov. 2022 09:55:21"), }), factory.bootResource({ arch: "amd64", name: "ubuntu/focal", title: "20.04 LTS", - lastDeployed: "Thu, 17 Nov. 2022 09:55:21", + lastDeployed: factory.timestamp("Thu, 17 Nov. 2022 09:55:21"), machineCount: 768, }), factory.bootResource({ name: "ubuntu/bionic", arch: "i386", title: "18.04 LTS", - lastDeployed: "Wed, 18 Nov. 2022 08:55:21", + lastDeployed: factory.timestamp("Wed, 18 Nov. 2022 08:55:21"), }), ]; const state = factory.rootState({ @@ -465,20 +465,20 @@ it("sorts by release by default", () => { arch: "amd64", name: "ubuntu/focal", title: "20.04 LTS", - lastDeployed: "Thu, 17 Nov. 2022 09:55:21", + lastDeployed: factory.timestamp("Thu, 17 Nov. 2022 09:55:21"), machineCount: 768, }), factory.bootResource({ name: "ubuntu/bionic", arch: "i386", title: "18.04 LTS", - lastDeployed: "Wed, 18 Nov. 2022 08:55:21", + lastDeployed: factory.timestamp("Wed, 18 Nov. 2022 08:55:21"), }), factory.bootResource({ name: "ubuntu/xenial", arch: "amd64", title: "16.04 LTS", - lastDeployed: "Tue, 16 Nov. 2022 09:55:21", + lastDeployed: factory.timestamp("Tue, 16 Nov. 2022 09:55:21"), }), ]; const state = factory.rootState({ diff --git a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx index cdb9d899676..c33c16aab8f 100644 --- a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx +++ b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx @@ -17,17 +17,13 @@ describe("DhcpForm", () => { dhcpsnippet: factory.dhcpSnippetState({ items: [ factory.dhcpSnippet({ - created: "Thu, 15 Aug. 2019 06:21:39", id: 1, name: "lease", - updated: "Thu, 15 Aug. 2019 06:21:39", value: "lease 10", }), factory.dhcpSnippet({ - created: "Thu, 15 Aug. 2019 06:21:39", id: 2, name: "class", - updated: "Thu, 15 Aug. 2019 06:21:39", }), ], loaded: true, diff --git a/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx b/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx index 16d2f7d5c11..69c379a7e75 100644 --- a/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx +++ b/src/app/settings/views/Dhcp/DhcpList/DhcpList.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { ContentSection } from "@canonical/maas-react-components"; import { Code, Col, Row } from "@canonical/react-components"; -import { format, parse } from "date-fns"; import { useDispatch, useSelector } from "react-redux"; import type { Dispatch } from "redux"; @@ -34,6 +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"; const getTargetName = ( controllers: Controller[], @@ -80,10 +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 - ? format( - parse(dhcpsnippet.updated, "E, dd LLL. yyyy HH:mm:ss", new Date()), - "yyyy-LL-dd H:mm" - ) + ? formatUtcTimestamp(dhcpsnippet.updated) : "Never"; const enabled = dhcpsnippet.enabled ? "Yes" : "No"; const showDelete = expandedType === "delete"; diff --git a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx index 7b330e44aca..7ed4129d5ac 100644 --- a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx +++ b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx @@ -193,8 +193,8 @@ describe("RepositoryForm", () => { const store = mockStore(state); const repository = { id: 9, - created: "Tue, 27 Aug. 2019 12:39:12", - updated: "Tue, 27 Aug. 2019 12:39:12", + created: factory.timestamp("Tue, 27 Aug. 2019 12:39:12"), + updated: factory.timestamp("Tue, 27 Aug. 2019 12:39:12"), name: "name", url: "http://www.website.com", distributions: [], diff --git a/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx b/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx index cfab712df3a..8bb51eb836b 100644 --- a/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx +++ b/src/app/settings/views/Scripts/ScriptsList/ScriptsList.test.tsx @@ -300,7 +300,7 @@ describe("ScriptsList", () => { loaded: true, items: [ factory.script({ - created: "Thu, 31 Dec. 2020 22:59:00", + created: factory.timestamp("Thu, 31 Dec. 2020 22:59:00"), script_type: ScriptType.TESTING, }), ], @@ -326,7 +326,7 @@ describe("ScriptsList", () => { loaded: true, items: [ factory.script({ - created: "", + created: factory.timestamp(""), script_type: ScriptType.TESTING, }), ], diff --git a/src/app/settings/views/Users/UsersList/UsersList.tsx b/src/app/settings/views/Users/UsersList/UsersList.tsx index 4e4b6c69e33..66d235a145f 100644 --- a/src/app/settings/views/Users/UsersList/UsersList.tsx +++ b/src/app/settings/views/Users/UsersList/UsersList.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { ContentSection } from "@canonical/maas-react-components"; import { Notification } from "@canonical/react-components"; -import { format, parse } from "date-fns"; import { useDispatch, useSelector } from "react-redux"; import TableActions from "@/app/base/components/TableActions"; @@ -23,6 +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"; type SortKey = keyof User; @@ -35,10 +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 - ? format( - parse(user.last_login, "E, dd LLL. yyyy HH:mm:ss", new Date()), - "yyyy-LL-dd H:mm" - ) + ? formatUtcTimestamp(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 6d8e4bb39cc..08402eeb724 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 } from "@/app/store/types/model"; +import type { Model, UtcTimestamp } 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: string; - lastDeployed: string; + lastUpdate: UtcTimestamp; + lastDeployed: UtcTimestamp; 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 6468345e289..e9768088684 100644 --- a/src/app/store/controller/types/base.ts +++ b/src/app/store/controller/types/base.ts @@ -8,7 +8,11 @@ import type { import type { APIError } from "@/app/base/types"; import type { CertificateMetadata, PowerType } from "@/app/store/general/types"; import type { PowerState, StorageLayout } from "@/app/store/types/enum"; -import type { ModelRef, TimestampFields } from "@/app/store/types/model"; +import type { + ModelRef, + TimestampFields, + UtcTimestamp, +} from "@/app/store/types/model"; import type { NodeActions, BaseNode, @@ -65,7 +69,7 @@ export type ControllerActions = export type BaseController = BaseNode & { actions: ControllerActions[]; - last_image_sync: string; + last_image_sync: UtcTimestamp; link_type: NodeLinkType.CONTROLLER; node_type_display: | NodeTypeDisplay.RACK_CONTROLLER diff --git a/src/app/store/machine/types/base.ts b/src/app/store/machine/types/base.ts index 23f0c91de10..e265e42406a 100644 --- a/src/app/store/machine/types/base.ts +++ b/src/app/store/machine/types/base.ts @@ -5,7 +5,11 @@ import type { ActionState, APIError, Seconds } from "@/app/base/types"; import type { CloneError } from "@/app/machines/components/MachineForms/MachineActionFormWrapper/CloneForm/CloneResults/CloneResults"; import type { CertificateMetadata, PowerType } from "@/app/store/general/types"; import type { PowerState, StorageLayout } from "@/app/store/types/enum"; -import type { ModelRef, TimestampFields } from "@/app/store/types/model"; +import type { + ModelRef, + TimestampFields, + UtcTimestamp, +} from "@/app/store/types/model"; import type { BaseNode, Disk, @@ -69,7 +73,7 @@ export type MachineDetails = BaseMachine & bmc: number; boot_disk: Disk | null; certificate?: CertificateMetadata; - commissioning_start_time: string; + commissioning_start_time: UtcTimestamp; commissioning_status: TestStatus; cpu_speed: BaseNode["cpu_speed"]; cpu_test_status: TestStatus; @@ -122,8 +126,8 @@ type HardwareSyncFields = } | { enable_hw_sync: true; - last_sync: string; - next_sync: string; + last_sync: UtcTimestamp; + next_sync: UtcTimestamp; 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 bf494a68cfb..b81a85567b4 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 } from "@/app/store/types/model"; +import type { Model, UtcTimestamp } 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?: string; + updated?: UtcTimestamp; }; export type ScriptResult = PartialScriptResult & { @@ -76,7 +76,7 @@ export type ScriptResult = PartialScriptResult & { results: ScriptResultResult[]; script?: number; script_version?: number | null; - started?: string; + started?: UtcTimestamp; tags: string; }; diff --git a/src/app/store/types/model.ts b/src/app/store/types/model.ts index ee3ec4aac90..8d48cdd8fe7 100644 --- a/src/app/store/types/model.ts +++ b/src/app/store/types/model.ts @@ -2,9 +2,11 @@ export type Model = { id: number; }; +// Expected format: "Thu, 15 Aug. 2019 06:21:39" or "" +export type UtcTimestamp = string & { readonly __brand: unique symbol }; export type TimestampFields = { - created: string; - updated: string; + created: UtcTimestamp; + updated: UtcTimestamp; }; export type TimestampedModel = Model & TimestampFields; diff --git a/src/app/store/user/types/base.ts b/src/app/store/user/types/base.ts index f7b6c6c099e..8891dbfef2d 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 } from "@/app/store/types/model"; +import type { Model, UtcTimestamp } 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: string; + last_login: UtcTimestamp; machines_count: number; sshkeys_count: number; username: string; diff --git a/src/app/subnets/views/SubnetDetails/SubnetUsedIPs/SubnetUsedIPs.test.tsx b/src/app/subnets/views/SubnetDetails/SubnetUsedIPs/SubnetUsedIPs.test.tsx index f8e1081b243..6eec4d69916 100644 --- a/src/app/subnets/views/SubnetDetails/SubnetUsedIPs/SubnetUsedIPs.test.tsx +++ b/src/app/subnets/views/SubnetDetails/SubnetUsedIPs/SubnetUsedIPs.test.tsx @@ -12,18 +12,14 @@ const mockStore = configureStore(); it("displays correct IP addresses", () => { const subnet = factory.subnetDetails({ ip_addresses: [ - { + factory.subnetIP({ ip: "11.1.1.1", alloc_type: 4, - created: "yesterday", - updated: "today", - }, - { + }), + factory.subnetIP({ ip: "11.1.1.2", alloc_type: 5, - created: "yesterday", - updated: "today", - }, + }), ], }); const state = factory.rootState({ diff --git a/src/app/utils/time.test.ts b/src/app/utils/time.test.ts index 9aa4b5aa24d..62944bbd326 100644 --- a/src/app/utils/time.test.ts +++ b/src/app/utils/time.test.ts @@ -1,7 +1,14 @@ import MockDate from "mockdate"; import timezoneMock from "timezone-mock"; -import { formatUtcDatetime, getTimeDistanceString } from "./time"; +import { + formatUtcTimestamp, + getTimeDistanceString, + getUtcTimestamp, +} from "./time"; + +import type { UtcTimestamp } from "@/app/store/types/model"; +import * as factory from "@/testing/factories"; beforeEach(() => { MockDate.set("Fri, 18 Nov. 2022 01:01:00"); @@ -13,27 +20,41 @@ afterEach(() => { describe("getTimeDistanceString", () => { it("returns time distance for UTC TimeString in the past", () => { - expect(getTimeDistanceString("Fri, 18 Nov. 2022 01:00:50")).toEqual( - "less than a minute ago" - ); + expect( + getTimeDistanceString("Fri, 18 Nov. 2022 01:00:50" as UtcTimestamp) + ).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")).toEqual( - "in less than a minute" - ); + expect( + getTimeDistanceString("Fri, 18 Nov. 2022 01:01:10" as UtcTimestamp) + ).toEqual("in less than a minute"); }); }); -describe("formatUtcDatetime", () => { +describe("formatUtcTimestamp", () => { it("returns UTC date time in a correct format", () => { - expect(formatUtcDatetime("Fri, 18 Nov. 2022 01:00:50")).toEqual( - "Fri, 18 Nov. 2022 01:00:50" - ); + expect( + formatUtcTimestamp("Fri, 18 Nov. 2022 01:00:50" as UtcTimestamp) + ).toEqual("Fri, 18 Nov. 2022 01:00:50"); }); it("returns UTC date time in local time", () => { timezoneMock.register("Etc/GMT-1"); - expect(formatUtcDatetime("Fri, 18 Nov. 2022 03:00:00")).toEqual( - "Fri, 18 Nov. 2022 04:00:00" - ); + expect( + formatUtcTimestamp("Fri, 18 Nov. 2022 03:00:00" as UtcTimestamp) + ).toEqual("Fri, 18 Nov. 2022 04:00:00"); + }); +}); + +describe("getUtcTimestamp", () => { + it("appends (UTC) to the given time string", () => { + const inputTimeString = "Fri, 18 Nov. 2022 01:00:50" as UtcTimestamp; + const expectedOutput = "Fri, 18 Nov. 2022 01:00:50 (UTC)"; + expect(getUtcTimestamp(inputTimeString)).toEqual(expectedOutput); + }); + + 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); }); }); diff --git a/src/app/utils/time.ts b/src/app/utils/time.ts index 71ff73a5a2d..e342ecf8223 100644 --- a/src/app/utils/time.ts +++ b/src/app/utils/time.ts @@ -1,23 +1,40 @@ import { format, formatDistance, parse } from "date-fns"; +import type { UtcTimestamp } from "@/app/store/types/model"; + const DATETIME_FORMAT = "E, dd LLL. yyyy HH:mm:ss"; const UTC_DATETIME_FORMAT = `${DATETIME_FORMAT} x`; -export const parseUtcDatetime = (utcTimeString: string): Date => +export const parseUtcDatetime = (utcTimeString: UtcTimestamp): Date => parse( `${utcTimeString} +00`, // let parse fn know it's UTC UTC_DATETIME_FORMAT, new Date() ); -export const getTimeDistanceString = (utcTimeString: string): string => +export const getTimeDistanceString = (utcTimeString: UtcTimestamp): string => formatDistance(parseUtcDatetime(utcTimeString), new Date(), { addSuffix: true, }); /** - * @param utcTimeString - time string in UTC_DATETIME_FORMAT - * @returns time string adjusted for local time zone in DATETIME_FORMAT + * Appends "(UTC)" to a given string to indicate the time zone explicitly. + * @param utcTimeString - time string in UTC + * @returns Time string with appended "(UTC)" + */ +export const getUtcTimestamp = (utcTimeString?: UtcTimestamp): string => + utcTimeString ? utcTimeString + " (UTC)" : ""; + +/** + * 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 formatUtcDatetime = (utcTimeString: string): string => - format(parseUtcDatetime(utcTimeString), DATETIME_FORMAT); +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)" + : ""; diff --git a/src/testing/factories/bootresource.ts b/src/testing/factories/bootresource.ts index 68380cc62ba..03d9791139a 100644 --- a/src/testing/factories/bootresource.ts +++ b/src/testing/factories/bootresource.ts @@ -1,5 +1,6 @@ import { define, extend } from "cooky-cutter"; +import { timestamp } from "./general"; import { model } from "./model"; import type { @@ -34,8 +35,8 @@ export const bootResource = extend(model, { downloading: false, machineCount: 0, numberOfNodes: 0, - lastUpdate: "Tue, 08 Jun. 2021 02:12:47", - lastDeployed: "Tue, 08 Jun. 2021 02:12:47", + lastUpdate: timestamp("Tue, 08 Jun. 2021 02:12:47"), + lastDeployed: timestamp("Tue, 08 Jun. 2021 02:12:47"), canDeployToMemory: true, }); diff --git a/src/testing/factories/dhcpsnippet.ts b/src/testing/factories/dhcpsnippet.ts index e40c4b20311..dacc89ab1e2 100644 --- a/src/testing/factories/dhcpsnippet.ts +++ b/src/testing/factories/dhcpsnippet.ts @@ -7,9 +7,10 @@ import type { DHCPSnippetHistory, } from "@/app/store/dhcpsnippet/types"; import type { Model, TimestampedModel } from "@/app/store/types/model"; +import { timestamp } from "@/testing/factories"; export const dhcpSnippetHistory = extend(model, { - created: "Wed, 08 Jul. 2020 05:35:4", + created: timestamp("Wed, 08 Jul. 2020 05:35:04"), value: "test value", }); diff --git a/src/testing/factories/general.ts b/src/testing/factories/general.ts index 9802943713a..ceb20ab38b3 100644 --- a/src/testing/factories/general.ts +++ b/src/testing/factories/general.ts @@ -29,6 +29,7 @@ import { PowerFieldScope, PowerFieldType, } from "@/app/store/general/types"; +import type { UtcTimestamp } from "@/app/store/types/model"; import { NodeActions } from "@/app/store/types/node"; export const architecture = define("amd64"); @@ -126,3 +127,7 @@ export const tlsCertificate = define({ }); export const version = define("test version"); + +export const timestamp = define( + "Wed, 08 Jul. 2020 05:35:4" as UtcTimestamp +); diff --git a/src/testing/factories/index.ts b/src/testing/factories/index.ts index f9b331ab199..a087bd6355f 100644 --- a/src/testing/factories/index.ts +++ b/src/testing/factories/index.ts @@ -170,6 +170,7 @@ export { powerField, powerFieldChoice, powerType, + timestamp, tlsCertificate, version, } from "./general"; diff --git a/src/testing/factories/model.ts b/src/testing/factories/model.ts index 2c0cdef8c07..78daf66c4dd 100644 --- a/src/testing/factories/model.ts +++ b/src/testing/factories/model.ts @@ -1,5 +1,7 @@ import { define, extend, random, sequence } from "cooky-cutter"; +import { timestamp } from "./general"; + import type { Model, ModelRef, @@ -11,8 +13,8 @@ export const model = define({ }); export const timestampedModel = extend(model, { - created: "Wed, 19 Feb. 2020 11:59:19", - updated: "Fri, 03 Jul. 2020 02:44:12", + created: timestamp("Wed, 19 Feb. 2020 11:59:19"), + updated: timestamp("Fri, 03 Jul. 2020 02:44:12"), }); export const modelRef = extend(model, { diff --git a/src/testing/factories/nodes.ts b/src/testing/factories/nodes.ts index a354158e0f1..a6b604f07d9 100644 --- a/src/testing/factories/nodes.ts +++ b/src/testing/factories/nodes.ts @@ -1,5 +1,6 @@ import { define, extend, random, sequence } from "cooky-cutter"; +import { timestamp } from "./general"; import { model, modelRef, timestampedModel } from "./model"; import type { @@ -211,7 +212,7 @@ export const deviceInterface = extend( ); export const deviceDetails = extend(device, { - created: "Thu, 15 Oct. 2020 07:25:10", + created: timestamp("Thu, 15 Oct. 2020 07:25:10"), description: "Device description", interfaces: () => [deviceInterface()], locked: false, @@ -219,7 +220,7 @@ export const deviceDetails = extend(device, { on_network: false, pool: null, swap_size: null, - updated: "Thu, 15 Oct. 2020 07:25:10", + updated: timestamp("Thu, 15 Oct. 2020 07:25:10"), }); const node = extend(simpleNode, { @@ -307,7 +308,7 @@ export const machineEventType = extend(model, { }); export const machineEvent = extend(model, { - created: "Mon, 19 Oct. 2020 07:04:37", + created: timestamp("Mon, 19 Oct. 2020 07:04:37"), description: "smartctl-validate on name-VZJoCN timed out", type: machineEventType, }); @@ -335,9 +336,9 @@ export const machineDetails = extend(machine, { bios_boot_method: "uefi", bmc: 190, boot_disk: null, - commissioning_start_time: "Thu, 15 Oct. 2020 07:25:10", + commissioning_start_time: timestamp("Thu, 15 Oct. 2020 07:25:10"), commissioning_status: testStatus, - created: "Thu, 15 Oct. 2020 07:25:10", + created: timestamp("Thu, 15 Oct. 2020 07:25:10"), current_commissioning_script_set: 6188, current_installation_script_set: 6174, current_testing_script_set: 6192, @@ -388,13 +389,13 @@ export const machineDetails = extend(machine, { supported_filesystems: () => [], cpu_speed: 1000, swap_size: null, - updated: "Fri, 23 Oct. 2020 05:24:41", + updated: timestamp("Fri, 23 Oct. 2020 05:24:41"), }); export const controller = extend(node, { actions, description: "a test controller", - last_image_sync: "Thu, 02 Jul. 2020 22:55:00", + last_image_sync: timestamp("Thu, 02 Jul. 2020 22:55:00"), link_type: NodeLinkType.CONTROLLER, node_type_display: NodeTypeDisplay.REGION_AND_RACK_CONTROLLER, node_type: 4, @@ -412,7 +413,7 @@ export const controllerDetails = extend( boot_disk: null, commissioning_start_time: "Thu, 15 Oct. 2020 07:25:10", commissioning_status: testStatus, - created: "Thu, 15 Oct. 2020 07:25:10", + created: timestamp("Thu, 15 Oct. 2020 07:25:10"), current_commissioning_script_set: 6188, current_installation_script_set: 6174, current_testing_script_set: 6192, @@ -480,7 +481,7 @@ export const controllerDetails = extend( swap_size: null, testing_start_time: "Thu, 15 Oct. 2020 07:25:10", testing_status: testStatus, - updated: "Fri, 23 Oct. 2020 05:24:41", + updated: timestamp("Fri, 23 Oct. 2020 05:24:41"), vault_configured: false, vlan: null, vlan_ids: () => [], diff --git a/src/testing/factories/scriptResult.ts b/src/testing/factories/scriptResult.ts index 6fe12cb3fa8..acd3af72125 100644 --- a/src/testing/factories/scriptResult.ts +++ b/src/testing/factories/scriptResult.ts @@ -1,5 +1,6 @@ import { array, define, extend } from "cooky-cutter"; +import { timestamp } from "./general"; import { model } from "./model"; import type { @@ -26,7 +27,7 @@ export const partialScriptResult = extend(model, { status: 2, status_name: "test status", suppressed: false, - updated: "Fri, 13 Nov. 2020 04:50:27", + updated: timestamp("Fri, 13 Nov. 2020 04:50:27"), }); export const scriptResult = extend(model, { @@ -44,13 +45,13 @@ export const scriptResult = extend(model, { runtime: "0:00:00", script: 1, script_version: 2, - started: "Fri, 13 Nov. 2020 04:50:26", + started: timestamp("Fri, 13 Nov. 2020 04:50:26"), starttime: 605243026.966467, status: 2, status_name: "test status", suppressed: false, tags: "test, tags", - updated: "Fri, 13 Nov. 2020 04:50:27", + updated: timestamp("Fri, 13 Nov. 2020 04:50:27"), }); export const scriptResultData = define({ diff --git a/src/testing/factories/subnet.ts b/src/testing/factories/subnet.ts index 26f79307a16..edddad2f01e 100644 --- a/src/testing/factories/subnet.ts +++ b/src/testing/factories/subnet.ts @@ -1,5 +1,6 @@ import { array, define, extend, random } from "cooky-cutter"; +import { timestamp } from "./general"; import { model, timestampedModel } from "./model"; import { PodType } from "@/app/store/pod/constants"; @@ -68,9 +69,9 @@ export const subnetIPNodeSummary = define({ export const subnetIP = define({ alloc_type: IPAddressType.AUTO, - created: "Wed, 08 Jul. 2020 05:35:4", + created: timestamp("Wed, 08 Jul. 2020 05:35:04"), ip: "192.168.1.1", - updated: "Wed, 08 Jul. 2020 05:35:4", + updated: timestamp("Wed, 08 Jul. 2020 05:35:04"), }); export const subnetScanFailure = define({ diff --git a/src/testing/factories/user.ts b/src/testing/factories/user.ts index 61e0c997cac..735fd7f10b6 100644 --- a/src/testing/factories/user.ts +++ b/src/testing/factories/user.ts @@ -1,5 +1,6 @@ import { define, extend } from "cooky-cutter"; +import { timestamp } from "./general"; import { model } from "./model"; import type { Model } from "@/app/store/types/model"; @@ -15,7 +16,7 @@ export const user = extend(model, { is_local: true, is_superuser: true, last_name: "Full Name jr.", - last_login: "Fri, 23 Oct. 2020 00:00:00", + last_login: timestamp("Fri, 23 Oct. 2020 00:00:00"), sshkeys_count: 3, username: (i: number) => `user${i}`, machines_count: 1,