From 2ce6d9c5e26cbbf4446cb329b2e0b869555b2128 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Tue, 17 Oct 2023 19:17:43 +0300 Subject: [PATCH 1/4] fix: decode logs in results object and remove overflow of caused by text Closes #1436 fix: show errors --- .../Playbooks/Runs/PlaybookRunsActions.tsx | 4 +- .../Playbooks/Runs/PlaybookRunsSidePanel.tsx | 1 + .../Runs/PlaybooksActionsResults.tsx | 33 ++++++++++--- .../PlaybooksActionsResults.unit.test.tsx | 49 +++++++++++++++++++ 4 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 src/components/Playbooks/Runs/__tests__/PlaybooksActionsResults.unit.test.tsx diff --git a/src/components/Playbooks/Runs/PlaybookRunsActions.tsx b/src/components/Playbooks/Runs/PlaybookRunsActions.tsx index d5d26de97..5d6bab1f2 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsActions.tsx +++ b/src/components/Playbooks/Runs/PlaybookRunsActions.tsx @@ -97,9 +97,9 @@ export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) { ))} -
+
{selectedAction && ( -
+
)} diff --git a/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx b/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx index 7792f447d..52b3c2ef1 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx +++ b/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx @@ -51,6 +51,7 @@ export type PlaybookRunAction = { end_time?: string; result?: { stdout?: string; + logs?: string; [key: string]: unknown; }; error?: string; diff --git a/src/components/Playbooks/Runs/PlaybooksActionsResults.tsx b/src/components/Playbooks/Runs/PlaybooksActionsResults.tsx index fb9e14a01..cdd70163f 100644 --- a/src/components/Playbooks/Runs/PlaybooksActionsResults.tsx +++ b/src/components/Playbooks/Runs/PlaybooksActionsResults.tsx @@ -1,21 +1,40 @@ import { PlaybookRunAction } from "./PlaybookRunsSidePanel"; +import Convert from "ansi-to-html"; + +const convert = new Convert(); type Props = { - action: Pick; + action: Pick; + className?: string; }; -export default function PlaybooksRunActionsResults({ action }: Props) { - const { result } = action; +export default function PlaybooksRunActionsResults({ + action, + className = "whitespace-pre-wrap break-all" +}: Props) { + const { result, error } = action; - if (!result) { + if (!result && !error) { return <>No result; } - if (result.stdout) { - return
{result.stdout}
; + if (action.error) { + return
{action.error}
; + } + + if (result?.stdout) { + return
{result.stdout}
; + } + + if (result?.logs) { + const html = convert.toHtml(result.logs); + + return ( +
+    );
   }
 
   const json = JSON.stringify(result, null, 2);
 
-  return 
{json}
; + return
{json}
; } diff --git a/src/components/Playbooks/Runs/__tests__/PlaybooksActionsResults.unit.test.tsx b/src/components/Playbooks/Runs/__tests__/PlaybooksActionsResults.unit.test.tsx new file mode 100644 index 000000000..6a218fe09 --- /dev/null +++ b/src/components/Playbooks/Runs/__tests__/PlaybooksActionsResults.unit.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from "@testing-library/react"; +import PlaybooksRunActionsResults from "../PlaybooksActionsResults"; + +describe("PlaybooksRunActionsResults", () => { + it("renders 'No result' when result is falsy", () => { + render(); + expect(screen.getByText("No result")).toBeInTheDocument(); + }); + + it("renders stdout when result has stdout", () => { + const action = { result: { stdout: "Hello, world!" } }; + render(); + expect(screen.getByText("Hello, world!")).toBeInTheDocument(); + }); + + it("renders logs when result has logs", () => { + const action = { result: { logs: "Hello, world!" } }; + render(); + expect(screen.getByText("Hello, world!")).toBeInTheDocument(); + }); + + it("renders JSON when result has neither stdout nor logs", () => { + const action = { result: { foo: "bar" } }; + render(); + expect( + screen.getByText('{ "foo": "bar" }', { + exact: false + }) + ).toBeInTheDocument(); + }); + + it("applies the className prop to the pre element", () => { + const action = { result: { stdout: "Hello, world!" } }; + render( + + ); + expect(screen.getByText("Hello, world!")).toHaveClass("text-red-500"); + }); + + it("renders error when action has error", () => { + const action = { error: "Something went wrong" }; + render(); + expect( + screen.getByText("Something went wrong", { + exact: false + }) + ).toBeInTheDocument(); + }); +}); From 9c3209865509385c7daf77604223683996f7d122 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 18 Oct 2023 09:26:50 +0300 Subject: [PATCH 2/4] refactor: restructure directory and rename a few files --- .../{ => Actions}/PlaybookRunsActionItem.tsx | 6 +-- .../PlaybookRunsActions.stories.tsx | 0 .../{ => Actions}/PlaybookRunsActions.tsx | 17 +++---- .../PlaybooksActionsResults.stories.tsx | 0 .../{ => Actions}/PlaybooksActionsResults.tsx | 2 +- .../PlaybooksActionsResults.unit.test.tsx | 0 .../Playbooks/Runs/PlaybookRunTypes.tsx | 50 +++++++++++++++++++ .../Playbooks/Runs/PlaybookRunsList.tsx | 8 +-- .../Playbooks/Runs/PlaybookRunsSidePanel.tsx | 46 ++--------------- .../Playbooks/Runs/PlaybookRunsStatus.tsx | 2 +- src/pages/playbooks/PlaybookRunsDetails.tsx | 2 +- 11 files changed, 70 insertions(+), 63 deletions(-) rename src/components/Playbooks/Runs/{ => Actions}/PlaybookRunsActionItem.tsx (86%) rename src/components/Playbooks/Runs/{ => Actions}/PlaybookRunsActions.stories.tsx (100%) rename src/components/Playbooks/Runs/{ => Actions}/PlaybookRunsActions.tsx (86%) rename src/components/Playbooks/Runs/{ => Actions}/PlaybooksActionsResults.stories.tsx (100%) rename src/components/Playbooks/Runs/{ => Actions}/PlaybooksActionsResults.tsx (93%) rename src/components/Playbooks/Runs/{ => Actions}/__tests__/PlaybooksActionsResults.unit.test.tsx (100%) create mode 100644 src/components/Playbooks/Runs/PlaybookRunTypes.tsx diff --git a/src/components/Playbooks/Runs/PlaybookRunsActionItem.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsActionItem.tsx similarity index 86% rename from src/components/Playbooks/Runs/PlaybookRunsActionItem.tsx rename to src/components/Playbooks/Runs/Actions/PlaybookRunsActionItem.tsx index 158ac8624..936bd11c0 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsActionItem.tsx +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunsActionItem.tsx @@ -1,6 +1,6 @@ -import { relativeDateTime } from "../../../utils/date"; -import { PlaybookRunAction } from "./PlaybookRunsSidePanel"; -import PlaybookRunsStatus from "./PlaybookRunsStatus"; +import { relativeDateTime } from "../../../../utils/date"; +import { PlaybookRunAction } from "../PlaybookRunTypes"; +import PlaybookRunsStatus from "../PlaybookRunsStatus"; type PlaybookRunsActionItemProps = { action: PlaybookRunAction; diff --git a/src/components/Playbooks/Runs/PlaybookRunsActions.stories.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.stories.tsx similarity index 100% rename from src/components/Playbooks/Runs/PlaybookRunsActions.stories.tsx rename to src/components/Playbooks/Runs/Actions/PlaybookRunsActions.stories.tsx diff --git a/src/components/Playbooks/Runs/PlaybookRunsActions.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx similarity index 86% rename from src/components/Playbooks/Runs/PlaybookRunsActions.tsx rename to src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx index 5d6bab1f2..5a72d285c 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsActions.tsx +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx @@ -1,14 +1,13 @@ import { ReactNode, useMemo, useState } from "react"; import { Link } from "react-router-dom"; -import { relativeDateTime } from "../../../utils/date"; -import { Avatar } from "../../Avatar"; -import { Icon } from "../../Icon"; +import { relativeDateTime } from "../../../../utils/date"; +import { Avatar } from "../../../Avatar"; +import { Icon } from "../../../Icon"; +import { PlaybookRun, PlaybookRunAction } from "../PlaybookRunTypes"; +import PlaybookRunsStatus from "./../PlaybookRunsStatus"; import PlaybookRunsActionItem from "./PlaybookRunsActionItem"; -import { PlaybookRunAction } from "./PlaybookRunsSidePanel"; -import PlaybookRunsStatus from "./PlaybookRunsStatus"; -import PlaybooksRunActionsResults from "./PlaybooksActionsResults"; -export type PlaybookRunWithActions = PlaybookRunAction & { +export type PlaybookRunWithActions = PlaybookRun & { actions: PlaybookRunAction[]; }; @@ -99,9 +98,7 @@ export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) {
{selectedAction && ( -
- -
+
)}
diff --git a/src/components/Playbooks/Runs/PlaybooksActionsResults.stories.tsx b/src/components/Playbooks/Runs/Actions/PlaybooksActionsResults.stories.tsx similarity index 100% rename from src/components/Playbooks/Runs/PlaybooksActionsResults.stories.tsx rename to src/components/Playbooks/Runs/Actions/PlaybooksActionsResults.stories.tsx diff --git a/src/components/Playbooks/Runs/PlaybooksActionsResults.tsx b/src/components/Playbooks/Runs/Actions/PlaybooksActionsResults.tsx similarity index 93% rename from src/components/Playbooks/Runs/PlaybooksActionsResults.tsx rename to src/components/Playbooks/Runs/Actions/PlaybooksActionsResults.tsx index cdd70163f..e5bb8d3f3 100644 --- a/src/components/Playbooks/Runs/PlaybooksActionsResults.tsx +++ b/src/components/Playbooks/Runs/Actions/PlaybooksActionsResults.tsx @@ -1,5 +1,5 @@ -import { PlaybookRunAction } from "./PlaybookRunsSidePanel"; import Convert from "ansi-to-html"; +import { PlaybookRunAction } from "../PlaybookRunTypes"; const convert = new Convert(); diff --git a/src/components/Playbooks/Runs/__tests__/PlaybooksActionsResults.unit.test.tsx b/src/components/Playbooks/Runs/Actions/__tests__/PlaybooksActionsResults.unit.test.tsx similarity index 100% rename from src/components/Playbooks/Runs/__tests__/PlaybooksActionsResults.unit.test.tsx rename to src/components/Playbooks/Runs/Actions/__tests__/PlaybooksActionsResults.unit.test.tsx diff --git a/src/components/Playbooks/Runs/PlaybookRunTypes.tsx b/src/components/Playbooks/Runs/PlaybookRunTypes.tsx new file mode 100644 index 000000000..866d9e67f --- /dev/null +++ b/src/components/Playbooks/Runs/PlaybookRunTypes.tsx @@ -0,0 +1,50 @@ +import { ConfigItem } from "../../../api/services/configs"; +import { User } from "../../../api/services/users"; +import { Topology } from "../../../context/TopologyPageContext"; +import { HealthCheck } from "../../../types/healthChecks"; +import { PlaybookSpec } from "../Settings/PlaybookSpecsTable"; + +export type PlaybookRunStatus = + | "scheduled" + | "running" + | "cancelled" + | "completed" + | "failed" + | "pending"; + +export type PlaybookRunAction = { + id: string; + name: string; + status: PlaybookRunStatus; + playbook_run_id: string; + start_time: string; + scheduled_time?: string; + end_time?: string; + result?: { + stdout?: string; + logs?: string; + [key: string]: unknown; + }; + error?: string; +}; + +export type PlaybookRun = { + id: string; + playbook_id?: string; + status: PlaybookRunStatus; + start_time: string; + scheduled_time?: string; + end_time?: string; + created_at?: string; + created_by?: User; + check_id?: string; + config_id?: string; + component_id?: string; + parameters?: Record; + agent_id?: string; + /* relationships */ + playbooks?: PlaybookSpec; + component?: Pick; + check?: Pick; + config?: Pick; +}; diff --git a/src/components/Playbooks/Runs/PlaybookRunsList.tsx b/src/components/Playbooks/Runs/PlaybookRunsList.tsx index 91488ea58..ad3f2ea99 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsList.tsx +++ b/src/components/Playbooks/Runs/PlaybookRunsList.tsx @@ -7,10 +7,10 @@ import { Avatar } from "../../Avatar"; import { DateCell } from "../../ConfigViewer/columns"; import { DataTable, PaginationOptions } from "../../DataTable"; import { Icon } from "../../Icon"; -import { PlaybookRunAction, PlaybookRunStatus } from "./PlaybookRunsSidePanel"; +import { PlaybookRun, PlaybookRunStatus } from "./PlaybookRunTypes"; import PlaybookRunsStatus from "./PlaybookRunsStatus"; -const playbookRunsTableColumns: ColumnDef[] = [ +const playbookRunsTableColumns: ColumnDef[] = [ { header: "Name", accessorKey: "name", @@ -86,7 +86,7 @@ const playbookRunsTableColumns: ColumnDef[] = [ ]; type Props = { - data: PlaybookRunAction[]; + data: PlaybookRun[]; isLoading?: boolean; pagination?: PaginationOptions; } & Omit, "data">; @@ -100,7 +100,7 @@ export default function PlaybookRunsTable({ const navigate = useNavigate(); const onRowClick = useCallback( - (row: PlaybookRunAction) => { + (row: PlaybookRun) => { navigate(`/playbooks/runs/${row.id}`); }, [navigate] diff --git a/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx b/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx index 52b3c2ef1..5909cd894 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx +++ b/src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx @@ -4,10 +4,7 @@ import { useAtom } from "jotai"; import { useEffect, useMemo, useState } from "react"; import { AiOutlineTeam } from "react-icons/ai"; import { useNavigate } from "react-router-dom"; -import { ConfigItem } from "../../../api/services/configs"; import { getPlaybookRuns } from "../../../api/services/playbooks"; -import { Topology } from "../../../context/TopologyPageContext"; -import { HealthCheck } from "../../../types/healthChecks"; import { relativeDateTime } from "../../../utils/date"; import PillBadge from "../../Badge/PillBadge"; import CollapsiblePanel from "../../CollapsiblePanel"; @@ -16,8 +13,7 @@ import { InfiniteTable } from "../../InfiniteTable/InfiniteTable"; import TextSkeletonLoader from "../../SkeletonLoader/TextSkeletonLoader"; import { refreshButtonClickedTrigger } from "../../SlidingSideBar"; import Title from "../../Title/title"; -import { PlaybookSpec } from "../Settings/PlaybookSpecsTable"; -import { User } from "../../../api/services/users"; +import { PlaybookRun } from "./PlaybookRunTypes"; type TopologySidePanelProps = { panelType: "topology"; @@ -34,49 +30,13 @@ type Props = { onCollapsedStateChange?: (isClosed: boolean) => void; }; -export type PlaybookRunStatus = - | "scheduled" - | "running" - | "cancelled" - | "completed" - | "failed" - | "pending"; - -export type PlaybookRunAction = { - id: string; - name?: string; - status: PlaybookRunStatus; - playbook_run_id?: string; - start_time: string; - end_time?: string; - result?: { - stdout?: string; - logs?: string; - [key: string]: unknown; - }; - error?: string; - playbooks?: PlaybookSpec; - playbook_id?: string; - created_at?: string; - created_by?: User; - check_id?: string; - config_id?: string; - component_id?: string; - parameters?: Record; - agent_id?: string; - component?: Pick; - check?: Pick; - config?: Pick; -}; - -const runsColumns: ColumnDef[] = [ +const runsColumns: ColumnDef[] = [ { header: "Name", id: "name", - accessorKey: "name", size: 60, cell: ({ row }) => { - const name = row.original.name ?? row.original.playbooks?.name; + const name = row.original.playbooks?.name; return {name}; } }, diff --git a/src/components/Playbooks/Runs/PlaybookRunsStatus.tsx b/src/components/Playbooks/Runs/PlaybookRunsStatus.tsx index 8f3641d7f..899b72c7f 100644 --- a/src/components/Playbooks/Runs/PlaybookRunsStatus.tsx +++ b/src/components/Playbooks/Runs/PlaybookRunsStatus.tsx @@ -1,6 +1,6 @@ import { BsFillExclamationCircleFill } from "react-icons/bs"; import { FaCheckCircle, FaClock, FaSpinner } from "react-icons/fa"; -import { PlaybookRunStatus } from "./PlaybookRunsSidePanel"; +import { PlaybookRunStatus } from "./PlaybookRunTypes"; const statusIconMap: Record = { completed: { diff --git a/src/pages/playbooks/PlaybookRunsDetails.tsx b/src/pages/playbooks/PlaybookRunsDetails.tsx index 674970c98..6c741014b 100644 --- a/src/pages/playbooks/PlaybookRunsDetails.tsx +++ b/src/pages/playbooks/PlaybookRunsDetails.tsx @@ -7,7 +7,7 @@ import { BreadcrumbRoot } from "../../components/BreadcrumbNav"; import { SearchLayout } from "../../components/Layout"; -import PlaybookRunsActions from "../../components/Playbooks/Runs/PlaybookRunsActions"; +import PlaybookRunsActions from "../../components/Playbooks/Runs/Actions/PlaybookRunsActions"; import CardsSkeletonLoader from "../../components/SkeletonLoader/CardsSkeletonLoader"; import { Head } from "../../components/Head/Head"; import { playbookRunsPageTabs } from "../../components/Playbooks/Runs/PlaybookRunsPageTabs"; From 05c9efd9e8af7702e0b301d7a5c3e70f5369041d Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 18 Oct 2023 09:31:00 +0300 Subject: [PATCH 3/4] fix: fetch actions on demand and not before --- src/api/services/playbooks.ts | 18 +++++++++++---- .../Runs/Actions/PlaybookRunActionFetch.tsx | 22 +++++++++++++++++++ .../Runs/Actions/PlaybookRunsActions.tsx | 5 ++++- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/components/Playbooks/Runs/Actions/PlaybookRunActionFetch.tsx diff --git a/src/api/services/playbooks.ts b/src/api/services/playbooks.ts index 7a3fd858f..5e2dc70c6 100644 --- a/src/api/services/playbooks.ts +++ b/src/api/services/playbooks.ts @@ -1,5 +1,8 @@ -import { PlaybookRunWithActions } from "../../components/Playbooks/Runs/PlaybookRunsActions"; -import { PlaybookRunAction } from "../../components/Playbooks/Runs/PlaybookRunsSidePanel"; +import { PlaybookRunWithActions } from "../../components/Playbooks/Runs/Actions/PlaybookRunsActions"; +import { + PlaybookRun, + PlaybookRunAction +} from "../../components/Playbooks/Runs/PlaybookRunTypes"; import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm"; import { NewPlaybookSpec, @@ -70,7 +73,14 @@ export async function getPlaybookToRunForResource( export async function getPlaybookRun(id: string) { const res = await IncidentCommander.get( - `/playbook_runs?id=eq.${id}&select=*,created_by(${AVATAR_INFO}),playbooks(id,name),component:components(id,name,icon),actions:playbook_run_actions(*)` + `/playbook_runs?id=eq.${id}&select=*,created_by(${AVATAR_INFO}),playbooks(id,name),component:components(id,name,icon),actions:playbook_run_actions(id,name,status,start_time,end_time)` + ); + return res.data?.[0] ?? undefined; +} + +export async function getPlaybookRunActionById(id: string) { + const res = await IncidentCommander.get( + `/playbook_run_actions?id=eq.${id}&select=*` ); return res.data?.[0] ?? undefined; } @@ -94,7 +104,7 @@ export async function getPlaybookRuns({ const pagingParams = `&limit=${pageSize}&offset=${pageIndex * pageSize}`; const res = await resolve( - ConfigDB.get( + ConfigDB.get( `/playbook_runs?select=*,playbooks(id,name),component:components(id,name,icon),check:checks(id,name,icon),config:config_items(id,name,type,config_class)&order=created_at.desc${componentParamString}&${configParamString}${pagingParams}}`, { headers: { diff --git a/src/components/Playbooks/Runs/Actions/PlaybookRunActionFetch.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunActionFetch.tsx new file mode 100644 index 000000000..a35da032b --- /dev/null +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunActionFetch.tsx @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; +import { getPlaybookRunActionById } from "../../../../api/services/playbooks"; +import TableSkeletonLoader from "../../../SkeletonLoader/TableSkeletonLoader"; +import PlaybooksRunActionsResults from "./PlaybooksActionsResults"; + +type Props = { + playbookRunActionId: string; +}; + +export default function PlaybookRunActionFetch({ playbookRunActionId }: Props) { + const { data: action, isLoading } = useQuery({ + queryKey: ["playbookRunAction", playbookRunActionId], + queryFn: () => getPlaybookRunActionById(playbookRunActionId), + enabled: !!playbookRunActionId + }); + + if (isLoading || !action) { + return ; + } + + return ; +} diff --git a/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx index 5a72d285c..7629c5a1f 100644 --- a/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx @@ -5,6 +5,7 @@ import { Avatar } from "../../../Avatar"; import { Icon } from "../../../Icon"; import { PlaybookRun, PlaybookRunAction } from "../PlaybookRunTypes"; import PlaybookRunsStatus from "./../PlaybookRunsStatus"; +import PlaybookRunActionFetch from "./PlaybookRunActionFetch"; import PlaybookRunsActionItem from "./PlaybookRunsActionItem"; export type PlaybookRunWithActions = PlaybookRun & { @@ -98,7 +99,9 @@ export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) {
{selectedAction && ( -
+
+ +
)}
From d6e1b2742b66d30df71a6b53126f585b5ca499fc Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 18 Oct 2023 09:47:27 +0300 Subject: [PATCH 4/4] fix: return duration in milliseconds when less than a second --- src/utils/date.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/date.ts b/src/utils/date.ts index a1fcc31c1..9ca957b47 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -22,7 +22,7 @@ dayjs.updateLocale("en", { relativeTime: { future: "in %s", past: "%s ago", - s: "seconds", + s: "%ds", m: "1m", mm: "%dm", h: "1h", @@ -132,6 +132,12 @@ export const relativeDateTime = (from: string | Date, to?: string | Date) => { const fromDate = dayjs.utc(from).local(); if (to) { const toDate = dayjs.utc(to).local(); + // if the difference is less than 1 second, return difference in + // milliseconds. This is to avoid returning 0 seconds when DateTime.from + // from dayjs. + if (toDate.diff(fromDate) < 1000) { + return `${toDate.diff(fromDate)}ms`; + } return fromDate.from(toDate, true); } return fromDate.fromNow();