From ab263bb76a36ac832bedabfca16b424acc616a13 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 11 Sep 2023 09:45:15 -0400 Subject: [PATCH] Fleet UI: [small released bug] Fix time cell with tooltip component to not have a date restriction, add unit tests for edge cases (#13824) --- changes/14346-fix-cve-time-ago | 1 + .../HumanTimeDiffWithDateTip.tests.tsx | 36 ++++++++++ .../HumanTimeDiffWithDateTip.tsx | 70 ++++++++++--------- .../ActivityItem/ActivityItem.tsx | 21 ++---- .../Vulnerabilities/Vulnerabilities.tsx | 1 - frontend/utilities/constants.ts | 3 + frontend/utilities/helpers.ts | 47 ++++++++++--- 7 files changed, 121 insertions(+), 58 deletions(-) create mode 100644 changes/14346-fix-cve-time-ago create mode 100644 frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tests.tsx diff --git a/changes/14346-fix-cve-time-ago b/changes/14346-fix-cve-time-ago new file mode 100644 index 0000000000..e1333f1ea0 --- /dev/null +++ b/changes/14346-fix-cve-time-ago @@ -0,0 +1 @@ +- Fleet UI: Fix software vulnerabilities time ago column for old CVEs diff --git a/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tests.tsx b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tests.tsx new file mode 100644 index 0000000000..2f06815930 --- /dev/null +++ b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tests.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { renderWithSetup } from "test/test-utils"; + +import HumanTimeDiffWithDateTip from "./HumanTimeDiffWithDateTip"; + +const EMPTY_STRING = "Unavailable"; +const INVALID_STRING = "Invalid date"; + +describe("HumanTimeDiffWithDateTip - component", () => { + it("renders tooltip on hover", async () => { + const { user } = renderWithSetup( + + ); + + // Note: number of years varies over time + await user.hover(screen.getByText(/years ago/i)); + + // Note: hour of day varies for timezones + expect(screen.getByText(/12\/6\/2015/i)).toBeInTheDocument(); + }); + + it("handles empty string error", async () => { + render(); + + const emptyStringText = screen.getByText(EMPTY_STRING); + expect(emptyStringText).toBeInTheDocument(); + }); + + it("handles invalid string error", async () => { + render(); + + const invalidStringText = screen.getByText(INVALID_STRING); + expect(invalidStringText).toBeInTheDocument(); + }); +}); diff --git a/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx index eb0259fd4f..75d00e1a8b 100644 --- a/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx +++ b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx @@ -1,40 +1,44 @@ import React from "react"; import { uniqueId } from "lodash"; -import { humanHostLastSeen } from "utilities/helpers"; +import { humanLastSeen, internationalTimeFormat } from "utilities/helpers"; import ReactTooltip from "react-tooltip"; -import intlFormat from "date-fns/intlFormat"; -export default ({ timeString }: { timeString: string }): JSX.Element => { +interface IHumanTimeDiffWithDateTip { + timeString: string; +} + +/** Returns "Unavailable" if date is empty string or "Unavailable" + * Returns "Invalid date" if date is invalid */ +export default ({ timeString }: IHumanTimeDiffWithDateTip): JSX.Element => { const id = uniqueId(); - return timeString === "Unavailable" ? ( - Unavailable - ) : ( - <> - - {humanHostLastSeen(timeString)} - - - {intlFormat( - new Date(timeString), - { - year: "numeric", - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }, - { locale: window.navigator.languages[0] } - )} - - - ); + + if (timeString === "Unavailable" || timeString === "") { + return Unavailable; + } + + try { + return ( + <> + + {humanLastSeen(timeString)} + + + {internationalTimeFormat(new Date(timeString))} + + + ); + } catch (e) { + if (e instanceof RangeError) { + return Invalid date; + } + return Unavailable; + } }; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index 47be44efb8..2ef3d66715 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -1,16 +1,18 @@ import React from "react"; import { find, lowerCase, noop } from "lodash"; -import { intlFormat, formatDistanceToNowStrict } from "date-fns"; +import { formatDistanceToNowStrict } from "date-fns"; import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity"; -import { addGravatarUrlToResource } from "utilities/helpers"; +import { + addGravatarUrlToResource, + internationalTimeFormat, +} from "utilities/helpers"; import { DEFAULT_GRAVATAR_LINK } from "utilities/constants"; import Avatar from "components/Avatar"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import ReactTooltip from "react-tooltip"; import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; -import { act } from "react-dom/test-utils"; const baseClass = "activity-item"; @@ -705,18 +707,7 @@ const ActivityItem = ({ id={`activity-${activity.id}`} backgroundColor="#3e4771" > - {intlFormat( - activityCreatedAt, - { - year: "numeric", - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }, - { locale: window.navigator.languages[0] } - )} + {internationalTimeFormat(activityCreatedAt)}

diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx index 138996eeb5..0446c6f20d 100644 --- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx +++ b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx @@ -50,7 +50,6 @@ const Vulnerabilities = ({ return (

Vulnerabilities

- {software?.vulnerabilities?.length ? ( <> {software && ( diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index 51cda895c2..14530e240b 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -39,6 +39,9 @@ export const FREQUENCY_DROPDOWN_OPTIONS = [ export const GITHUB_NEW_ISSUE_LINK = "https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md"; +/** July 28, 2016 is the date of the initial commit to fleet/fleet. */ +export const INITIAL_FLEET_DATE = "2016-07-28T00:00:00Z"; + export const LOGGING_TYPE_OPTIONS = [ { label: "Snapshot", value: "snapshot" }, { label: "Differential", value: "differential" }, diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts index 72d09475dd..77d9b6dba7 100644 --- a/frontend/utilities/helpers.ts +++ b/frontend/utilities/helpers.ts @@ -15,9 +15,10 @@ import { buildQueryStringFromParams } from "utilities/url"; import md5 from "js-md5"; import { formatDistanceToNow, - isAfter, - intervalToDuration, formatDuration, + intlFormat, + intervalToDuration, + isAfter, } from "date-fns"; import yaml from "js-yaml"; @@ -44,6 +45,7 @@ import { DEFAULT_GRAVATAR_LINK_FALLBACK, DEFAULT_GRAVATAR_LINK_DARK, DEFAULT_GRAVATAR_LINK_DARK_FALLBACK, + INITIAL_FLEET_DATE, PLATFORM_LABEL_DISPLAY_TYPES, } from "utilities/constants"; import { IScheduledQueryStats } from "interfaces/scheduled_query_stats"; @@ -586,7 +588,7 @@ export const humanHostLastRestart = ( !detailUpdatedAt || !uptime || detailUpdatedAt === DEFAULT_EMPTY_CELL_VALUE || - detailUpdatedAt < "2016-07-28T00:00:00Z" || + detailUpdatedAt < INITIAL_FLEET_DATE || typeof uptime !== "number" ) { return "Unavailable"; @@ -613,7 +615,7 @@ export const humanHostLastRestart = ( }; export const humanHostLastSeen = (lastSeen: string): string => { - if (!lastSeen || lastSeen < "2016-07-28T00:00:00Z") { + if (!lastSeen || lastSeen < INITIAL_FLEET_DATE) { return "Never"; } if (lastSeen === "Unavailable") { @@ -623,7 +625,7 @@ export const humanHostLastSeen = (lastSeen: string): string => { }; export const humanHostEnrolled = (enrolled: string): string => { - if (!enrolled || enrolled < "2016-07-28T00:00:00Z") { + if (!enrolled || enrolled < INITIAL_FLEET_DATE) { return "Never"; } return formatDistanceToNow(new Date(enrolled), { addSuffix: true }); @@ -636,8 +638,7 @@ export const humanHostMemory = (bytes: number): string => { export const humanHostDetailUpdated = (detailUpdated?: string): string => { // Handles the case when a host has checked in to Fleet but // its details haven't been updated. - // July 28, 2016 is the date of the initial commit to fleet/fleet. - if (!detailUpdated || detailUpdated < "2016-07-28T00:00:00Z") { + if (!detailUpdated || detailUpdated < INITIAL_FLEET_DATE) { return "unavailable"; } try { @@ -647,6 +648,33 @@ export const humanHostDetailUpdated = (detailUpdated?: string): string => { } }; +/** Unlike humanHost helper functions, there are no Fleet-related date restrictions */ +export const humanLastSeen = (lastSeen: string): string => { + if (!lastSeen) { + return "Never"; + } + if (lastSeen === "Unavailable") { + return "Unavailable"; + } + + return formatDistanceToNow(new Date(lastSeen), { addSuffix: true }); +}; + +export const internationalTimeFormat = (date: number | Date): string => { + return intlFormat( + date, + { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }, + { locale: window.navigator.languages[0] } + ); +}; + const MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES = { darwin: { enabled: @@ -687,8 +715,7 @@ export const hostTeamName = (teamName: string | null): string => { export const humanQueryLastRun = (lastRun: string): string => { // Handles the case when a query has never been ran. - // July 28, 2016 is the date of the initial commit to fleet/fleet. - if (!lastRun || lastRun < "2016-07-28T00:00:00Z") { + if (!lastRun || lastRun < INITIAL_FLEET_DATE) { return "Has not run"; } @@ -893,6 +920,8 @@ export default { humanHostEnrolled, humanHostMemory, humanHostDetailUpdated, + humanLastSeen, + internationalTimeFormat, getHostDiskEncryptionTooltipMessage, hostTeamName, humanQueryLastRun,