From cdefa0c9e9c63dfb16b23346d52fc9bb1e708015 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Mon, 20 Jan 2025 10:39:46 +0000 Subject: [PATCH] Chore rework UI activities (#25539) For #23912 new UI for activities on the global, past, and upcoming feeds. These are the same changes in [this PR](https://github.com/fleetdm/fleet/pull/25329), except we are reverting the changes around fleet initiated activities as that is not in the current activities API. We are doing this so that the new activities can go out in a release while the backend is still being built and will be ready later. > NOTE: this does contain the code for cancel activity functionality but it hidden from the user. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality --- changes/issue-23912-ui-for-activities | 1 + .../components/ActivityItem/ActivityItem.tsx | 192 +++++++++ frontend/components/ActivityItem/_styles.scss | 170 ++++++++ .../ActivityItem/index.ts | 0 frontend/components/Avatar/Avatar.stories.tsx | 6 + frontend/components/Avatar/Avatar.tests.tsx | 21 + frontend/components/Avatar/Avatar.tsx | 100 ++++- .../components/buttons/Button/_styles.scss | 6 +- frontend/components/icons/InfoOutline.tsx | 32 ++ frontend/components/icons/index.ts | 2 + frontend/interfaces/activity.ts | 11 +- .../cards/ActivityFeed/ActivityFeed.tsx | 18 +- .../ActivityFeed/ActivityItem/_styles.scss | 64 --- .../GlobalActivityItem.tests.tsx} | 206 ++++------ .../GlobalActivityItem.tsx} | 364 +++--------------- .../ActivityFeed/GlobalActivityItem/index.ts | 1 + .../HostDetailsPage/HostDetailsPage.tsx | 20 +- .../CancelActivityModal.tsx | 66 ++++ .../modals/CancelActivityModal/helpers.ts | 7 + .../modals/CancelActivityModal/index.ts | 1 + .../hosts/details/cards/Activity/Activity.tsx | 30 +- .../details/cards/Activity/ActivityConfig.tsx | 15 +- .../CanceledScriptActivityItem.tsx | 25 ++ .../CanceledScriptActivityItem/index.ts | 1 + .../CanceledSoftwareInstallActivityItem.tsx | 21 + .../index.ts | 1 + .../InstalledSoftwareActivityItem.tsx | 19 +- .../LockedHostActivityItem.tsx | 7 +- .../RanScriptActivityItem.tsx | 18 +- .../UnlockedHostActivityItem.tsx | 6 +- .../HostActivityItem/HostActivityItem.tsx | 94 ----- .../Activity/HostActivityItem/_styles.scss | 65 ---- .../cards/Activity/HostActivityItem/index.ts | 1 - .../PastActivityFeed/PastActivityFeed.tsx | 12 +- .../ShowDetailsButton/ShowDetailsButton.tsx | 33 -- .../Activity/ShowDetailsButton/_styles.scss | 5 - .../cards/Activity/ShowDetailsButton/index.ts | 1 - .../UpcomingActivityFeed.tsx | 14 +- frontend/services/entities/activities.ts | 5 + .../date_format/date_format.tests.ts | 19 +- frontend/utilities/date_format/index.ts | 9 +- frontend/utilities/endpoints.ts | 3 + 42 files changed, 922 insertions(+), 770 deletions(-) create mode 100644 changes/issue-23912-ui-for-activities create mode 100644 frontend/components/ActivityItem/ActivityItem.tsx create mode 100644 frontend/components/ActivityItem/_styles.scss rename frontend/{pages/DashboardPage/cards/ActivityFeed => components}/ActivityItem/index.ts (100%) create mode 100644 frontend/components/Avatar/Avatar.tests.tsx create mode 100644 frontend/components/icons/InfoOutline.tsx delete mode 100644 frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/_styles.scss rename frontend/pages/DashboardPage/cards/ActivityFeed/{ActivityItem/ActivityItem.tests.tsx => GlobalActivityItem/GlobalActivityItem.tests.tsx} (86%) rename frontend/pages/DashboardPage/cards/ActivityFeed/{ActivityItem/ActivityItem.tsx => GlobalActivityItem/GlobalActivityItem.tsx} (77%) create mode 100644 frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/index.ts create mode 100644 frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/CancelActivityModal.tsx create mode 100644 frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/helpers.ts create mode 100644 frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/index.ts create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/CanceledScriptActivityItem.tsx create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/index.ts create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/CanceledSoftwareInstallActivityItem.tsx create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/index.ts delete mode 100644 frontend/pages/hosts/details/cards/Activity/HostActivityItem/HostActivityItem.tsx delete mode 100644 frontend/pages/hosts/details/cards/Activity/HostActivityItem/_styles.scss delete mode 100644 frontend/pages/hosts/details/cards/Activity/HostActivityItem/index.ts delete mode 100644 frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/ShowDetailsButton.tsx delete mode 100644 frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/_styles.scss delete mode 100644 frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/index.ts diff --git a/changes/issue-23912-ui-for-activities b/changes/issue-23912-ui-for-activities new file mode 100644 index 000000000000..bd7f1a602ca4 --- /dev/null +++ b/changes/issue-23912-ui-for-activities @@ -0,0 +1 @@ +- update the UI a new activities design diff --git a/frontend/components/ActivityItem/ActivityItem.tsx b/frontend/components/ActivityItem/ActivityItem.tsx new file mode 100644 index 000000000000..01451e703ea8 --- /dev/null +++ b/frontend/components/ActivityItem/ActivityItem.tsx @@ -0,0 +1,192 @@ +import React from "react"; +import ReactTooltip from "react-tooltip"; +import classnames from "classnames"; + +import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity"; +import { + addGravatarUrlToResource, + internationalTimeFormat, +} from "utilities/helpers"; +import { DEFAULT_GRAVATAR_LINK } from "utilities/constants"; + +import Avatar from "components/Avatar"; + +import { COLORS } from "styles/var/colors"; +import { dateAgo } from "utilities/date_format"; +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; +import { noop } from "lodash"; + +const baseClass = "activity-item"; + +export interface IShowActivityDetailsData { + type: string; + details?: IActivityDetails; +} + +/** + * A handler that will show the details of an activity. This is used to pass + * the details of an activity to the parent component to show the details of + * the activity. + */ +export type ShowActivityDetailsHandler = ({ + type, + details, +}: IShowActivityDetailsData) => void; + +interface IActivityItemProps { + activity: IActivity; + children: React.ReactNode; + /** + * Set this to `true` when rendering only this activity by itself. This will + * change the styles for the activity item for solo rendering. + * @default false */ + isSoloActivity?: boolean; + /** + * Set this to `true` to hide the show details button and prevent from rendering. + * Not all activities can show details, so this is a way to hide the button. + * @default false + */ + hideShowDetails?: boolean; + /** + * Set this to `true` to hide the close button and prevent from rendering + * @default false + */ + hideCancel?: boolean; + /** + * Set this to `true` to disable the cancel button. It will still render but + * will not be clickable. + * @default false + */ + disableCancel?: boolean; + className?: string; + onShowDetails?: ShowActivityDetailsHandler; + onCancel?: () => void; +} + +/** + * A wrapper that will render all the common elements of a host activity item. + * This includes the avatar, the created at timestamp, and a dash to separate + * the activity items. The `children` will be the specific details of the activity + * implemented in the component that uses this wrapper. + */ +const ActivityItem = ({ + activity, + children, + className, + isSoloActivity, + hideShowDetails = false, + hideCancel = false, + disableCancel = false, + onShowDetails = noop, + onCancel = noop, +}: IActivityItemProps) => { + const { actor_email } = activity; + const { gravatar_url } = actor_email + ? addGravatarUrlToResource({ email: actor_email }) + : { gravatar_url: DEFAULT_GRAVATAR_LINK }; + + // wrapped just in case the date string does not parse correctly + let activityCreatedAt: Date | null = null; + try { + activityCreatedAt = new Date(activity.created_at); + } catch (e) { + activityCreatedAt = null; + } + + const classNames = classnames(baseClass, className, { + [`${baseClass}__solo-activity`]: isSoloActivity, + [`${baseClass}__no-details`]: hideShowDetails, + }); + + const onShowActivityDetails = () => { + onShowDetails({ type: activity.type, details: activity.details }); + }; + + const onCancelActivity = (e: React.MouseEvent) => { + e.stopPropagation(); + onCancel(); + }; + + // TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in + // the backend. For now, if all these fields are empty, then we assume it was + // Fleet-initiated. + let fleetInitiated = false; + if ( + !activity.actor_email && + !activity.actor_full_name && + (activity.type === ActivityType.InstalledSoftware || + activity.type === ActivityType.InstalledAppStoreApp || + activity.type === ActivityType.RanScript) + ) { + fleetInitiated = true; + } + + return ( +
+
+
+ +
+
+
+
+ + {children} + +
+ + {activityCreatedAt && dateAgo(activityCreatedAt)} + + {activityCreatedAt && ( + + {internationalTimeFormat(activityCreatedAt)} + + )} +
+
+ {!hideShowDetails && ( + + )} + {!hideCancel && ( + + )} +
+
+
+ ); +}; + +export default ActivityItem; diff --git a/frontend/components/ActivityItem/_styles.scss b/frontend/components/ActivityItem/_styles.scss new file mode 100644 index 000000000000..9ec63722c3aa --- /dev/null +++ b/frontend/components/ActivityItem/_styles.scss @@ -0,0 +1,170 @@ +.activity-item { + display: grid; // Grid system is used to create variable solid line lengths + grid-template-columns: 16px 16px 8px 1fr; + grid-template-rows: max-content; + + &__avatar-wrapper { + box-sizing: border-box; + display: grid; + grid-template-columns: 16px 16px; + grid-template-rows: 8px 32px 1fr; + grid-column-start: 1; + grid-column-end: 3; + + .avatar-wrapper { + grid-column-start: 1; + grid-column-end: 3; + grid-row-start: 2; + grid-row-end: 3; + } + } + + &__avatar-upper-dash { + border-right: 1px solid $ui-fleet-black-10; + grid-column-start: 1; + grid-column-end: 2; + grid-row-start: 1; + grid-row-end: 2; + } + + &__avatar-lower-dash { + border-right: 1px solid $ui-fleet-black-10; + grid-column-start: 1; + grid-column-end: 2; + grid-row-start: 3; + grid-row-end: 4; + } + + &__details-wrapper { + display: flex; + gap: $pad-medium; + justify-content: space-between; + align-items: center; + grid-column-start: 4; + grid-row-start: 1; + padding: $pad-small; + margin-bottom: $pad-large; + + .premium-icon-tip { + position: relative; + top: 4px; + padding-right: $pad-xsmall; + } + + .activity-details { + margin: 0; + line-height: 16px; + } + + &:hover { + border-radius: $border-radius-large; + background-color: $ui-off-white; + cursor: pointer; + + .activity-item__details-actions { + visibility: visible; + } + } + + .button { + height: 16px; + + &--icon svg { + padding: 0; + } + } + } + + &__details-actions { + visibility: hidden; + display: flex; + gap: $pad-medium; + } + + &__close-icon { + cursor: pointer; + &:hover { + svg { + path { + stroke: $core-vibrant-blue; + } + } + } + }; + + &__details-topline { + font-size: $x-small; + overflow-wrap: anywhere; + } + + &__details-content { + margin-right: $pad-xsmall; + } + + &__details-bottomline { + font-size: $xx-small; + color: $ui-fleet-black-50; + } + + &__show-query-icon { + margin-left: $pad-xsmall; + } + + &:first-child { + .activity-item__avatar-upper-dash { + border-right: none; + } + } + + &:last-child { + .activity-item__avatar-lower-dash { + border-right: none; + } + } + + /** + * Starting here are the styles for the activity item when it is the + * only activity that is being displayed (controlled by the `soloActivity prop`. + * We switch from grid to flexbox since we don't need the solid lines anymore. + * we also dont show to actions on hover + */ + &__solo-activity { + border: 1px solid $ui-fleet-black-10; + border-radius: $border-radius-large; + padding: $pad-medium; + display: flex; + gap: $pad-medium; + + .activity-item__avatar-wrapper { + display: block; + } + + .activity-item__avatar-lower-dash { + display: none; + } + + .activity-item__details-wrapper { + display: block; + padding: 0; + margin-bottom: 0; + + &:hover { + cursor: auto; + background-color: transparent; + } + } + + .activity-item__details-actions { + display: none; + } + } + + &__no-details { + .activity-item__details-wrapper { + &:hover { + cursor: auto; + background-color: transparent; + } + } + } +} diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/index.ts b/frontend/components/ActivityItem/index.ts similarity index 100% rename from frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/index.ts rename to frontend/components/ActivityItem/index.ts diff --git a/frontend/components/Avatar/Avatar.stories.tsx b/frontend/components/Avatar/Avatar.stories.tsx index 652e50b50f9f..e7d06f753060 100644 --- a/frontend/components/Avatar/Avatar.stories.tsx +++ b/frontend/components/Avatar/Avatar.stories.tsx @@ -30,3 +30,9 @@ export const Small: Story = { size: "small", }, }; + +export const UseFleetAvatar: Story = { + args: { + useFleetAvatar: true, + }, +}; diff --git a/frontend/components/Avatar/Avatar.tests.tsx b/frontend/components/Avatar/Avatar.tests.tsx new file mode 100644 index 000000000000..c28bee873bf4 --- /dev/null +++ b/frontend/components/Avatar/Avatar.tests.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import Avatar from "./Avatar"; + +describe("Avatar - component", () => { + it("renders the user gravatar if provided", () => { + render( + + ); + + const avatar = screen.getByAltText("User avatar"); + expect(avatar).toBeVisible(); + expect(avatar).toHaveAttribute("src", "https://example.com/avatar.png"); + }); + + it("renders the fleet avatar if useFleetAvatar is `true`", () => { + render(); + expect(screen.getByTestId("fleet-avatar")).toBeVisible(); + }); +}); diff --git a/frontend/components/Avatar/Avatar.tsx b/frontend/components/Avatar/Avatar.tsx index 319696be7bdb..13344f690807 100644 --- a/frontend/components/Avatar/Avatar.tsx +++ b/frontend/components/Avatar/Avatar.tsx @@ -3,16 +3,91 @@ import classnames from "classnames"; import { DEFAULT_GRAVATAR_LINK } from "utilities/constants"; +interface IFleetAvatarProps { + className?: string; +} + +/** + * a simple component that can be used to display a the Fleet logo as an avatar + */ +const FleetAvatar = ({ className }: IFleetAvatarProps) => { + return ( + + + + + + + + + + + + + + + + ); +}; + interface IAvatarUserInterface { gravatar_url?: string; gravatar_url_dark?: string; } -export interface IAvatarInterface { +interface IAvatarProps { className?: string; size?: string; user: IAvatarUserInterface; hasWhiteBackground?: boolean; + /** + * Set this to `true` to use the fleet avatar instead of the users gravatar. + */ + useFleetAvatar?: boolean; } const baseClass = "avatar"; @@ -22,7 +97,8 @@ const Avatar = ({ size, user, hasWhiteBackground, -}: IAvatarInterface): JSX.Element => { + useFleetAvatar = false, +}: IAvatarProps) => { const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); @@ -41,13 +117,19 @@ const Avatar = ({ return (
- User avatar + {useFleetAvatar ? ( + + ) : ( + User avatar + )}
); }; diff --git a/frontend/components/buttons/Button/_styles.scss b/frontend/components/buttons/Button/_styles.scss index db6a279e432a..f411d04f9c9c 100644 --- a/frontend/components/buttons/Button/_styles.scss +++ b/frontend/components/buttons/Button/_styles.scss @@ -230,18 +230,18 @@ $base-class: "button"; width: 100%; height: 100%; position: absolute; - border: 1px solid #6a67fe; + border: 1px solid $core-vibrant-blue; border-radius: 6px; } } &:hover, &:focus { - color: $core-vibrant-blue-over; + color: $core-vibrant-blue; svg { path { - fill: $core-vibrant-blue-over; + fill: $core-vibrant-blue; } } diff --git a/frontend/components/icons/InfoOutline.tsx b/frontend/components/icons/InfoOutline.tsx new file mode 100644 index 000000000000..abf744ac0979 --- /dev/null +++ b/frontend/components/icons/InfoOutline.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { COLORS, Colors } from "styles/var/colors"; +import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes"; + +interface IInfoOutlineProps { + size?: IconSizes; + color?: Colors; +} + +const InfoOutline = ({ + size = "medium", + color = "ui-fleet-black-75", +}: IInfoOutlineProps) => { + return ( + + + + ); +}; + +export default InfoOutline; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index df221cd38f8e..27d3ceb18f2e 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -66,6 +66,7 @@ import InstallSelfService from "./InstallSelfService"; import Settings from "./Settings"; import AutomaticSelfService from "./AutomaticSelfService"; import User from "./User"; +import InfoOutline from "./InfoOutline"; // a mapping of the usable names of icons to the icon source. export const ICON_MAP = { @@ -93,6 +94,7 @@ export const ICON_MAP = { "missing-hosts": MissingHosts, lightbulb: Lightbulb, info: Info, + "info-outline": InfoOutline, more: More, plus: Plus, policy: Policy, diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 71b1d01ccb60..e31355bc4d6d 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -97,18 +97,22 @@ export enum ActivityType { EnabledActivityAutomations = "enabled_activity_automations", EditedActivityAutomations = "edited_activity_automations", DisabledActivityAutomations = "disabled_activity_automations", + CanceledScript = "canceled_script", + CanceledSoftwareInstall = "canceled_software_install", } -// This is a subset of ActivityType that are shown only for the host past activities +/** This is a subset of ActivityType that are shown only for the host past activities */ export type IHostPastActivityType = | ActivityType.RanScript | ActivityType.LockedHost | ActivityType.UnlockedHost | ActivityType.InstalledSoftware | ActivityType.UninstalledSoftware - | ActivityType.InstalledAppStoreApp; + | ActivityType.InstalledAppStoreApp + | ActivityType.CanceledScript + | ActivityType.CanceledSoftwareInstall; -// This is a subset of ActivityType that are shown only for the host upcoming activities +/** This is a subset of ActivityType that are shown only for the host upcoming activities */ export type IHostUpcomingActivityType = | ActivityType.RanScript | ActivityType.InstalledSoftware @@ -132,6 +136,7 @@ export type IHostPastActivity = Omit & { }; export type IHostUpcomingActivity = Omit & { + uuid: string; type: IHostUpcomingActivityType; details: IActivityDetails; }; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index e4f63298b813..3755aced248d 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -19,8 +19,9 @@ import FleetIcon from "components/icons/FleetIcon"; import { AppInstallDetailsModal } from "components/ActivityDetails/InstallDetails/AppInstallDetails"; import { SoftwareInstallDetailsModal } from "components/ActivityDetails/InstallDetails/SoftwareInstallDetails/SoftwareInstallDetails"; import SoftwareUninstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal"; +import { IShowActivityDetailsData } from "components/ActivityItem/ActivityItem"; -import ActivityItem from "./ActivityItem"; +import GlobalActivityItem from "./GlobalActivityItem"; import ActivityAutomationDetailsModal from "./components/ActivityAutomationDetailsModal"; import RunScriptDetailsModal from "./components/RunScriptDetailsModal/RunScriptDetailsModal"; import SoftwareDetailsModal from "./components/SoftwareDetailsModal"; @@ -108,20 +109,17 @@ const ActivityFeed = ({ setPageIndex(pageIndex + 1); }; - const handleDetailsClick = ( - activityType: ActivityType, - details: IActivityDetails - ) => { - switch (activityType) { + const handleDetailsClick = ({ type, details }: IShowActivityDetailsData) => { + switch (type) { case ActivityType.LiveQuery: - queryShown.current = details.query_sql ?? ""; - queryImpact.current = details.stats + queryShown.current = details?.query_sql ?? ""; + queryImpact.current = details?.stats ? getPerformanceImpactDescription(details.stats) : undefined; setShowShowQueryModal(true); break; case ActivityType.RanScript: - scriptExecutionId.current = details.script_execution_id ?? ""; + scriptExecutionId.current = details?.script_execution_id ?? ""; setShowScriptDetailsModal(true); break; case ActivityType.InstalledSoftware: @@ -184,7 +182,7 @@ const ActivityFeed = ({ )}
{activities?.map((activity) => ( - { it("renders avatar, actor name, timestamp", async () => { @@ -17,7 +17,7 @@ describe("Activity Feed", () => { created_at: currentDate.toISOString(), }); - render(); + render(); // waiting for the activity data to render await screen.findByText("Test User"); @@ -31,7 +31,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.CreatedPack, }); - render(); + render(); expect(screen.getByText("created pack.")).toBeInTheDocument(); }); @@ -41,7 +41,7 @@ describe("Activity Feed", () => { type: ActivityType.CreatedPack, details: { pack_name: "Test pack" }, }); - render(); + render(); expect(screen.getByText("created pack .")).toBeInTheDocument(); expect(screen.getByText("Test pack")).toBeInTheDocument(); @@ -49,7 +49,7 @@ describe("Activity Feed", () => { it("renders a live_query type activity", () => { const activity = createMockActivity({ type: ActivityType.LiveQuery }); - render(); + render(); expect(screen.getByText("ran a live query .")).toBeInTheDocument(); }); @@ -61,7 +61,7 @@ describe("Activity Feed", () => { targets_count: 10, }, }); - render(); + render(); expect( screen.getByText("ran a live query on 10 hosts.") @@ -76,11 +76,10 @@ describe("Activity Feed", () => { query_sql: "SELECT * FROM users", }, }); - render(); + render(); expect(screen.getByText(/ran the/)).toBeInTheDocument(); expect(screen.getByText("Test Query")).toBeInTheDocument(); - expect(screen.getByText("Show query")).toBeInTheDocument(); }); it("renders a live_query type activity for a saved live query with targets and performance impact", () => { const activity = createMockActivity({ @@ -97,14 +96,13 @@ describe("Activity Feed", () => { }, }); - render(); + render(); expect(screen.getByText(/ran the/)).toBeInTheDocument(); expect(screen.getByText("Test Query")).toBeInTheDocument(); expect( screen.getByText(/with excessive performance impact on 10 hosts\./) ).toBeInTheDocument(); - expect(screen.getByText("Show query")).toBeInTheDocument(); }); it("renders a live_query type activity for a saved live query with targets and no performance impact", () => { @@ -122,19 +120,18 @@ describe("Activity Feed", () => { }, }); - render(); + render(); expect(screen.getByText(/ran the/)).toBeInTheDocument(); expect(screen.getByText("Test Query")).toBeInTheDocument(); expect(screen.queryByText(/Undetermined/)).toBeNull(); - expect(screen.getByText("Show query")).toBeInTheDocument(); }); it("renders an applied_spec_pack type activity", () => { const activity = createMockActivity({ type: ActivityType.AppliedSpecPack, }); - render(); + render(); expect( screen.getByText("edited a pack using fleetctl.") @@ -145,7 +142,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.AppliedSpecPolicy, }); - render(); + render(); expect( screen.getByText("edited policies using fleetctl.") @@ -156,7 +153,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.AppliedSpecSavedQuery, }); - render(); + render(); expect( screen.getByText("edited a query using fleetctl.") @@ -168,7 +165,7 @@ describe("Activity Feed", () => { type: ActivityType.AppliedSpecSavedQuery, details: { specs: [createMockQuery(), createMockQuery()] }, }); - render(); + render(); expect( screen.getByText("edited queries using fleetctl.") @@ -180,7 +177,7 @@ describe("Activity Feed", () => { type: ActivityType.AppliedSpecTeam, details: { teams: [createMockTeamSummary()] }, }); - render(); + render(); expect( screen.getByText("edited the team using fleetctl.") @@ -195,7 +192,7 @@ describe("Activity Feed", () => { teams: [createMockTeamSummary(), createMockTeamSummary()], }, }); - render(); + render(); expect( screen.getByText("edited multiple teams using fleetctl.") @@ -206,7 +203,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.UserAddedBySSO, }); - render(); + render(); expect(screen.getByText("was added to Fleet by SSO.")).toBeInTheDocument(); }); @@ -216,7 +213,7 @@ describe("Activity Feed", () => { type: ActivityType.EditedAgentOptions, details: { team_name: "Test Team 1" }, }); - render(); + render(); expect( screen.getByText("edited agent options on team.") @@ -229,7 +226,7 @@ describe("Activity Feed", () => { type: ActivityType.EditedAgentOptions, details: { global: true }, }); - render(); + render(); expect(screen.getByText("edited agent options.")).toBeInTheDocument(); }); @@ -239,7 +236,7 @@ describe("Activity Feed", () => { type: ActivityType.UserLoggedIn, details: { public_ip: "192.168.0.1" }, }); - render(); + render(); expect( screen.getByText("successfully logged in from public IP 192.168.0.1.") @@ -250,7 +247,7 @@ describe("Activity Feed", () => { type: ActivityType.UserLoggedIn, details: {}, }); - render(); + render(); expect(screen.getByText("successfully logged in.")).toBeInTheDocument(); }); @@ -260,7 +257,7 @@ describe("Activity Feed", () => { type: ActivityType.UserFailedLogin, details: { email: "foo@example.com", public_ip: "192.168.0.1" }, }); - render(); + render(); expect( screen.getByText(" failed to log in from public IP 192.168.0.1.", { @@ -281,7 +278,7 @@ describe("Activity Feed", () => { type: ActivityType.UserCreated, details: { user_email: "newuser@example.com" }, }); - render(); + render(); expect( screen.getByText("created a user", { exact: false }) @@ -298,7 +295,7 @@ describe("Activity Feed", () => { user_id: 3, }, }); - render(); + render(); // If actor_id is the same as user_id: // " activated their account." @@ -312,7 +309,7 @@ describe("Activity Feed", () => { type: ActivityType.UserDeleted, details: { user_email: "newuser@example.com" }, }); - render(); + render(); expect( screen.getByText("deleted a user", { exact: false }) @@ -329,7 +326,7 @@ describe("Activity Feed", () => { type: ActivityType.UserChangedGlobalRole, details: { user_email: "newuser@example.com", role: "maintainer" }, }); - render(); + render(); expect(screen.getByText("changed", { exact: false })).toBeInTheDocument(); expect(screen.getByText("newuser@example.com")).toBeInTheDocument(); @@ -344,7 +341,7 @@ describe("Activity Feed", () => { type: ActivityType.UserChangedGlobalRole, details: { user_email: "newuser@example.com", role: "maintainer" }, }); - render(); + render(); expect(screen.getByText("changed", { exact: false })).toBeInTheDocument(); expect(screen.getByText("newuser@example.com")).toBeInTheDocument(); @@ -363,7 +360,7 @@ describe("Activity Feed", () => { role: "observer", }, }); - render(); + render(); // If actor_id is the same as user_id: // " was assigned the for all teams." @@ -384,7 +381,7 @@ describe("Activity Feed", () => { role: "maintainer", }, }); - render(); + render(); // If actor_id is different from user_id on premium: // " changed to for all teams." @@ -407,7 +404,7 @@ describe("Activity Feed", () => { role: "maintainer", }, }); - render(); + render(); // If actor_id is different from user_id on free: // " changed to ." @@ -433,7 +430,7 @@ describe("Activity Feed", () => { team_name: "Test Team", }, }); - render(); + render(); expect(screen.getByText("changed", { exact: false })).toBeInTheDocument(); expect(screen.getByText("newuser@example.com")).toBeInTheDocument(); @@ -453,7 +450,7 @@ describe("Activity Feed", () => { team_name: "Test Team", }, }); - render(); + render(); // If actor_id is the same as user_id: // " was assigned the role for the team." @@ -481,7 +478,7 @@ describe("Activity Feed", () => { team_name: "Test Team", }, }); - render(); + render(); // If actor_id is different from user_id: // " changed to for the team." @@ -506,7 +503,7 @@ describe("Activity Feed", () => { team_name: "Test Team", }, }); - render(); + render(); expect(screen.getByText("removed", { exact: false })).toBeInTheDocument(); expect(screen.getByText("newuser@example.com")).toBeInTheDocument(); @@ -518,7 +515,7 @@ describe("Activity Feed", () => { type: ActivityType.UserDeletedGlobalRole, details: { user_email: "newuser@example.com", role: "maintainer" }, }); - render(); + render(); expect(screen.getByText("removed", { exact: false })).toBeInTheDocument(); expect(screen.getByText("newuser@example.com")).toBeInTheDocument(); @@ -533,7 +530,7 @@ describe("Activity Feed", () => { type: ActivityType.UserDeletedGlobalRole, details: { user_email: "newuser@example.com", role: "maintainer" }, }); - render(); + render(); expect(screen.getByText("removed", { exact: false })).toBeInTheDocument(); expect(screen.getByText("newuser@example.com")).toBeInTheDocument(); @@ -547,7 +544,7 @@ describe("Activity Feed", () => { type: ActivityType.EnabledDiskEncryption, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("enforced disk encryption for hosts assigned to the", { @@ -566,7 +563,7 @@ describe("Activity Feed", () => { type: ActivityType.EnabledMacDiskEncryption, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("enforced disk encryption for hosts assigned to the", { @@ -584,7 +581,7 @@ describe("Activity Feed", () => { type: ActivityType.DisabledMacDiskEncryption, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText( @@ -606,7 +603,7 @@ describe("Activity Feed", () => { type: ActivityType.DisabledDiskEncryption, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText( @@ -627,7 +624,7 @@ describe("Activity Feed", () => { type: ActivityType.EnabledDiskEncryption, details: {}, }); - render(); + render(); expect( screen.getByText("enforced disk encryption for hosts with no team.") @@ -641,7 +638,7 @@ describe("Activity Feed", () => { type: ActivityType.EnabledMacDiskEncryption, details: {}, }); - render(); + render(); expect( screen.getByText("enforced disk encryption for hosts with no team.") @@ -654,7 +651,7 @@ describe("Activity Feed", () => { type: ActivityType.DisabledDiskEncryption, details: {}, }); - render(); + render(); expect( screen.getByText( @@ -673,7 +670,7 @@ describe("Activity Feed", () => { type: ActivityType.DisabledMacDiskEncryption, details: {}, }); - render(); + render(); expect( screen.getByText( @@ -691,7 +688,7 @@ describe("Activity Feed", () => { type: ActivityType.ChangedMacOSSetupAssistant, details: { name: "dep-profile.json" }, }); - render(); + render(); expect( screen.getByText((content, node) => { @@ -708,7 +705,7 @@ describe("Activity Feed", () => { type: ActivityType.ChangedMacOSSetupAssistant, details: { name: "dep-profile.json", team_name: "Workstations" }, }); - render(); + render(); expect( screen.getByText((content, node) => { @@ -725,7 +722,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedMacOSSetupAssistant, details: { name: "dep-profile.json" }, }); - render(); + render(); expect( screen.getByText((content, node) => { @@ -742,7 +739,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedMacOSSetupAssistant, details: { name: "dep-profile.json", team_name: "Workstations" }, }); - render(); + render(); expect( screen.getByText((content, node) => { @@ -759,7 +756,7 @@ describe("Activity Feed", () => { type: ActivityType.AddedBootstrapPackage, details: { bootstrap_package_name: "foo.pkg", team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("added a bootstrap package (", { exact: false }) @@ -781,7 +778,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedBootstrapPackage, details: { bootstrap_package_name: "foo.pkg", team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("deleted a bootstrap package (", { exact: false }) @@ -803,7 +800,7 @@ describe("Activity Feed", () => { type: ActivityType.AddedBootstrapPackage, details: { bootstrap_package_name: "foo.pkg" }, }); - render(); + render(); expect( screen.getByText("added a bootstrap package (", { exact: false }) @@ -822,7 +819,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedBootstrapPackage, details: { bootstrap_package_name: "foo.pkg" }, }); - render(); + render(); expect( screen.getByText("deleted a bootstrap package (", { exact: false }) @@ -841,7 +838,7 @@ describe("Activity Feed", () => { type: ActivityType.EnabledMacOSSetupEndUserAuth, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText( @@ -858,7 +855,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.EnabledMacOSSetupEndUserAuth, }); - render(); + render(); expect( screen.getByText( @@ -873,7 +870,7 @@ describe("Activity Feed", () => { type: ActivityType.DisabledMacOSSetupEndUserAuth, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText( @@ -890,7 +887,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.DisabledMacOSSetupEndUserAuth, }); - render(); + render(); expect( screen.getByText( @@ -908,7 +905,7 @@ describe("Activity Feed", () => { host_display_names: ["foo"], }, }); - render(); + render(); expect( screen.getByText("transferred host", { exact: false }) @@ -926,7 +923,7 @@ describe("Activity Feed", () => { team_name: "Alphas", }, }); - render(); + render(); expect( screen.getByText("transferred host", { exact: false }) @@ -943,7 +940,7 @@ describe("Activity Feed", () => { host_display_names: ["foo", "bar", "baz"], }, }); - render(); + render(); expect( screen.getByText("transferred 3 hosts", { exact: false }) @@ -963,7 +960,7 @@ describe("Activity Feed", () => { team_name: "Alphas", }, }); - render(); + render(); expect( screen.getByText("transferred 3 hosts", { exact: false }) @@ -981,7 +978,7 @@ describe("Activity Feed", () => { host_serial: "ABCD", }, }); - render(); + render(); expect( screen.getByText((content, node) => { @@ -1002,7 +999,7 @@ describe("Activity Feed", () => { mdm_platform: "apple", }, }); - render(); + render(); expect( screen.getByText((content, node) => { @@ -1022,11 +1019,10 @@ describe("Activity Feed", () => { host_display_name: "ABCD", }, }); - render(); + render(); expect( screen.getByText((content, node) => { - console.log(node?.innerHTML); return ( node?.innerHTML === "Test User Mobile device management (MDM) was turned on for ABCD (manual)." @@ -1040,7 +1036,7 @@ describe("Activity Feed", () => { type: ActivityType.AddedScript, details: { script_name: "foo.sh", team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("added script ", { exact: false }) @@ -1062,7 +1058,7 @@ describe("Activity Feed", () => { type: ActivityType.EditedScript, details: { team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("edited scripts", { exact: false }) @@ -1085,7 +1081,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedScript, details: { script_name: "foo.sh", team_name: "Alphas" }, }); - render(); + render(); expect( screen.getByText("deleted script ", { exact: false }) @@ -1107,7 +1103,7 @@ describe("Activity Feed", () => { type: ActivityType.AddedScript, details: { script_name: "foo.sh" }, }); - render(); + render(); expect( screen.getByText("added script ", { exact: false }) @@ -1123,7 +1119,7 @@ describe("Activity Feed", () => { type: ActivityType.EditedScript, details: {}, }); - render(); + render(); expect( screen.getByText("edited scripts", { exact: false }) @@ -1138,7 +1134,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedScript, details: { script_name: "foo.sh" }, }); - render(); + render(); expect( screen.getByText("deleted script ", { exact: false }) @@ -1158,7 +1154,7 @@ describe("Activity Feed", () => { team_name: "Alphas", }, }); - render(); + render(); expect(screen.getByText("added", { exact: false })).toBeInTheDocument(); expect( @@ -1184,7 +1180,7 @@ describe("Activity Feed", () => { team_name: "Alphas", }, }); - render(); + render(); expect(screen.getByText("edited", { exact: false })).toBeInTheDocument(); expect( @@ -1207,7 +1203,7 @@ describe("Activity Feed", () => { team_name: "Alphas", }, }); - render(); + render(); expect(screen.getByText("deleted", { exact: false })).toBeInTheDocument(); expect( @@ -1229,7 +1225,7 @@ describe("Activity Feed", () => { type: ActivityType.AddedSoftware, details: { software_title: "Foo bar", software_package: "foobar.pkg" }, }); - render(); + render(); expect(screen.getByText("added", { exact: false })).toBeInTheDocument(); expect( @@ -1248,7 +1244,7 @@ describe("Activity Feed", () => { software_package: "foobar.pkg", }, }); - render(); + render(); expect(screen.getByText("edited", { exact: false })).toBeInTheDocument(); expect( @@ -1261,7 +1257,7 @@ describe("Activity Feed", () => { type: ActivityType.DeletedSoftware, details: { software_title: "Foo bar", software_package: "foobar.pkg" }, }); - render(); + render(); expect(screen.getByText("deleted", { exact: false })).toBeInTheDocument(); expect( @@ -1279,7 +1275,7 @@ describe("Activity Feed", () => { query_ids: [1, 2, 3], }, }); - render(); + render(); expect( screen.getByText("deleted multiple queries", { exact: false }) @@ -1293,7 +1289,7 @@ describe("Activity Feed", () => { host_display_name: "Foo Host", }, }); - render(); + render(); expect(screen.getByText("wiped", { exact: false })).toBeInTheDocument(); expect(screen.getByText("Foo Host", { exact: false })).toBeInTheDocument(); @@ -1310,7 +1306,7 @@ describe("Activity Feed", () => { }, }); - render(); + render(); expect(screen.getByText("Test Admin")).toBeInTheDocument(); }); @@ -1325,7 +1321,7 @@ describe("Activity Feed", () => { }, }); - render(); + render(); expect(screen.getByText("An end user")).toBeInTheDocument(); }); @@ -1340,7 +1336,7 @@ describe("Activity Feed", () => { }, }); - render(); + render(); expect(screen.getByText("Test Admin")).toBeInTheDocument(); }); @@ -1355,7 +1351,7 @@ describe("Activity Feed", () => { }, }); - render(); + render(); expect(screen.getByText("An end user")).toBeInTheDocument(); }); @@ -1363,7 +1359,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.AddedNdesScepProxy, }); - render(); + render(); expect(screen.getByText(/Test User/)).toBeInTheDocument(); expect( @@ -1377,7 +1373,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.EditedNdesScepProxy, }); - render(); + render(); expect(screen.getByText(/Test User/)).toBeInTheDocument(); expect( @@ -1391,7 +1387,7 @@ describe("Activity Feed", () => { const activity = createMockActivity({ type: ActivityType.DeletedNdesScepProxy, }); - render(); + render(); expect(screen.getByText(/Test User/)).toBeInTheDocument(); expect( @@ -1400,40 +1396,4 @@ describe("Activity Feed", () => { ) ).toBeInTheDocument(); }); - - it("renders setup experience installed software correctly", () => { - const activity = createMockActivity({ - type: ActivityType.InstalledSoftware, - actor_full_name: "", - actor_email: "", - actor_id: undefined, - }); - render(); - - expect(screen.getByText(/Fleet/)).toBeInTheDocument(); - }); - - it("renders setup experience ran script correctly", () => { - const activity = createMockActivity({ - type: ActivityType.RanScript, - actor_full_name: "", - actor_email: "", - actor_id: undefined, - }); - render(); - - expect(screen.getByText(/Fleet/)).toBeInTheDocument(); - }); - - it("renders setup experience installed VPP app correctly", () => { - const activity = createMockActivity({ - type: ActivityType.RanScript, - actor_full_name: "", - actor_email: "", - actor_id: undefined, - }); - render(); - - expect(screen.getByText(/Fleet/)).toBeInTheDocument(); - }); }); diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx similarity index 77% rename from frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx rename to frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index ef84fd1af08c..6d6004b1c464 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -1,29 +1,21 @@ import React from "react"; import { find, lowerCase, noop, trimEnd } from "lodash"; -import { formatDistanceToNowStrict } from "date-fns"; -import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity"; +import { ActivityType, IActivity } from "interfaces/activity"; import { getInstallStatusPredicate } from "interfaces/software"; import { AppleDisplayPlatform, PLATFORM_DISPLAY_NAMES, } from "interfaces/platform"; - import { - addGravatarUrlToResource, formatScriptNameForActivityItem, getPerformanceImpactDescription, - 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 { COLORS } from "styles/var/colors"; -const baseClass = "activity-item"; +import ActivityItem from "components/ActivityItem"; +import { ShowActivityDetailsHandler } from "components/ActivityItem/ActivityItem"; + +const baseClass = "global-activity-item"; const PREMIUM_ACTIVITIES = new Set([ "created_team", @@ -41,6 +33,18 @@ const PREMIUM_ACTIVITIES = new Set([ "disabled_windows_mdm_migration", ]); +const ACTIVITIES_WITH_DETAILS = new Set([ + ActivityType.RanScript, + ActivityType.AddedSoftware, + ActivityType.EditedSoftware, + ActivityType.DeletedSoftware, + ActivityType.InstalledSoftware, + ActivityType.UninstalledSoftware, + ActivityType.EnabledActivityAutomations, + ActivityType.EditedActivityAutomations, + ActivityType.LiveQuery, +]); + const getProfileMessageSuffix = ( isPremiumTier: boolean, platform: "apple" | "windows", @@ -96,16 +100,9 @@ const getMacOSSetupAssistantMessage = ( }; const TAGGED_TEMPLATES = { - liveQueryActivityTemplate: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { - targets_count: count, - query_name: queryName, - query_sql: querySql, - stats, - } = activity.details || {}; + liveQueryActivityTemplate: (activity: IActivity) => { + const { targets_count: count, query_name: queryName, stats } = + activity.details || {}; const impactDescription = stats ? getPerformanceImpactDescription(stats) @@ -135,23 +132,6 @@ const TAGGED_TEMPLATES = { ran {queryNameCopy} {impactCopy} {hostCountCopy}. - {querySql && ( - <> - - - )} ); }, @@ -683,32 +663,13 @@ const TAGGED_TEMPLATES = { ); }, - // TODO: Combine ranScript template with host details page templates - // frontend/pages/hosts/details/cards/Activity/PastActivity/PastActivity.tsx and - // frontend/pages/hosts/details/cards/Activity/UpcomingActivity/UpcomingActivity.tsx - ranScript: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { script_name, host_display_name, script_execution_id } = - activity.details || {}; + ranScript: (activity: IActivity) => { + const { script_name, host_display_name } = activity.details || {}; return ( <> {" "} ran {formatScriptNameForActivityItem(script_name)} on{" "} - {host_display_name}.{" "} - + {host_display_name}. ); }, @@ -892,18 +853,7 @@ const TAGGED_TEMPLATES = { ); }, - addedSoftware: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { - software_title, - software_package, - self_service, - labels_include_any, - labels_exclude_any, - } = activity.details || {}; - + addedSoftware: (activity: IActivity) => { return ( <> {" "} @@ -914,38 +864,11 @@ const TAGGED_TEMPLATES = { ) : ( "no team." - )}{" "} - + )} ); }, - editedSoftware: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { - software_title, - software_package, - self_service, - labels_include_any, - labels_exclude_any, - } = activity.details || {}; - + editedSoftware: (activity: IActivity) => { return ( <> {" "} @@ -956,38 +879,11 @@ const TAGGED_TEMPLATES = { ) : ( "no team." - )}{" "} - + )} ); }, - deletedSoftware: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { - software_title, - software_package, - self_service, - labels_include_any, - labels_exclude_any, - } = activity.details || {}; - + deletedSoftware: (activity: IActivity) => { return ( <> {" "} @@ -998,30 +894,11 @@ const TAGGED_TEMPLATES = { ) : ( "no team." - )}{" "} - + )} ); }, - installedSoftware: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { + installedSoftware: (activity: IActivity) => { const { details } = activity; if (!details) { return TAGGED_TEMPLATES.defaultActivityTemplate(activity); @@ -1042,22 +919,11 @@ const TAGGED_TEMPLATES = { {" "} {getInstallStatusPredicate(status)} {title} {showSoftwarePackage && ` (${details.software_package})`} on{" "} - {hostName}.{" "} - + {hostName}. ); }, - uninstalledSoftware: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { + uninstalledSoftware: (activity: IActivity) => { const { details } = activity; if (!details) { return TAGGED_TEMPLATES.defaultActivityTemplate(activity); @@ -1076,15 +942,7 @@ const TAGGED_TEMPLATES = { {" "} {getInstallStatusPredicate(status)} software {title} {showSoftwarePackage && ` (${details.software_package})`} from{" "} - {hostName}.{" "} - + {hostName}. ); }, @@ -1160,73 +1018,21 @@ const TAGGED_TEMPLATES = { ); }, - enabledActivityAutomations: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { webhook_url } = activity.details || {}; - return ( - <> - {" "} - enabled activity automations.{" "} - - - ); + enabledActivityAutomations: () => { + return <> enabled activity automations.; }, - editedActivityAutomations: ( - activity: IActivity, - onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void - ) => { - const { webhook_url } = activity.details || {}; - return ( - <> - {" "} - edited activity automations.{" "} - - - ); + editedActivityAutomations: () => { + return <> edited activity automations.; }, disabledActivityAutomations: () => { return <> disabled activity automations.; }, }; -const getDetail = ( - activity: IActivity, - isPremiumTier: boolean, - onDetailsClick?: ( - activityType: ActivityType, - details: IActivityDetails - ) => void -) => { +const getDetail = (activity: IActivity, isPremiumTier: boolean) => { switch (activity.type) { case ActivityType.LiveQuery: { - return TAGGED_TEMPLATES.liveQueryActivityTemplate( - activity, - onDetailsClick - ); + return TAGGED_TEMPLATES.liveQueryActivityTemplate(activity); } case ActivityType.AppliedSpecPack: { return TAGGED_TEMPLATES.editPackCtlActivityTemplate(); @@ -1364,7 +1170,7 @@ const getDetail = ( return TAGGED_TEMPLATES.disabledWindowsMdmMigration(); } case ActivityType.RanScript: { - return TAGGED_TEMPLATES.ranScript(activity, onDetailsClick); + return TAGGED_TEMPLATES.ranScript(activity); } case ActivityType.AddedScript: { return TAGGED_TEMPLATES.addedScript(activity); @@ -1409,19 +1215,19 @@ const getDetail = ( return TAGGED_TEMPLATES.resentConfigProfile(activity); } case ActivityType.AddedSoftware: { - return TAGGED_TEMPLATES.addedSoftware(activity, onDetailsClick); + return TAGGED_TEMPLATES.addedSoftware(activity); } case ActivityType.EditedSoftware: { - return TAGGED_TEMPLATES.editedSoftware(activity, onDetailsClick); + return TAGGED_TEMPLATES.editedSoftware(activity); } case ActivityType.DeletedSoftware: { - return TAGGED_TEMPLATES.deletedSoftware(activity, onDetailsClick); + return TAGGED_TEMPLATES.deletedSoftware(activity); } case ActivityType.InstalledSoftware: { - return TAGGED_TEMPLATES.installedSoftware(activity, onDetailsClick); + return TAGGED_TEMPLATES.installedSoftware(activity); } case ActivityType.UninstalledSoftware: { - return TAGGED_TEMPLATES.uninstalledSoftware(activity, onDetailsClick); + return TAGGED_TEMPLATES.uninstalledSoftware(activity); } case ActivityType.AddedAppStoreApp: { return TAGGED_TEMPLATES.addedAppStoreApp(activity); @@ -1430,7 +1236,7 @@ const getDetail = ( return TAGGED_TEMPLATES.deletedAppStoreApp(activity); } case ActivityType.InstalledAppStoreApp: { - return TAGGED_TEMPLATES.installedSoftware(activity, onDetailsClick); + return TAGGED_TEMPLATES.installedSoftware(activity); } case ActivityType.EnabledVpp: { return TAGGED_TEMPLATES.enabledVpp(activity); @@ -1439,16 +1245,10 @@ const getDetail = ( return TAGGED_TEMPLATES.disabledVpp(activity); } case ActivityType.EnabledActivityAutomations: { - return TAGGED_TEMPLATES.enabledActivityAutomations( - activity, - onDetailsClick - ); + return TAGGED_TEMPLATES.enabledActivityAutomations(); } case ActivityType.EditedActivityAutomations: { - return TAGGED_TEMPLATES.editedActivityAutomations( - activity, - onDetailsClick - ); + return TAGGED_TEMPLATES.editedActivityAutomations(); } case ActivityType.DisabledActivityAutomations: { return TAGGED_TEMPLATES.disabledActivityAutomations(); @@ -1463,28 +1263,20 @@ const getDetail = ( interface IActivityItemProps { activity: IActivity; isPremiumTier: boolean; - isSandboxMode?: boolean; /** A handler for handling clicking on the details of an activity. Not all * activites have more details so this is optional. An example of additonal * details is showing the query for a live query action. */ - onDetailsClick?: ( - activityType: ActivityType, - details: IActivityDetails - ) => void; + onDetailsClick?: ShowActivityDetailsHandler; } -const ActivityItem = ({ +const GlobalActivityItem = ({ activity, isPremiumTier, - isSandboxMode = false, onDetailsClick = noop, }: IActivityItemProps) => { - const { actor_email } = activity; - const { gravatar_url } = actor_email - ? addGravatarUrlToResource({ email: actor_email }) - : { gravatar_url: DEFAULT_GRAVATAR_LINK }; + const hasDetails = ACTIVITIES_WITH_DETAILS.has(activity.type); // Add the "Fleet" name to the activity if needed. // TODO: remove/refactor this once we have "fleet-initiated" activities. @@ -1498,10 +1290,6 @@ const ActivityItem = ({ activity.actor_full_name = "Fleet"; } - const activityCreatedAt = new Date(activity.created_at); - const indicatePremiumFeature = - isSandboxMode && PREMIUM_ACTIVITIES.has(activity.type); - const renderActivityPrefix = () => { const DEFAULT_ACTOR_DISPLAY = {activity.actor_full_name} ; @@ -1530,45 +1318,17 @@ const ActivityItem = ({ }; return ( -
- -
-
- {indicatePremiumFeature && } - - {renderActivityPrefix()} - {getDetail(activity, isPremiumTier, onDetailsClick)} - -
- - {formatDistanceToNowStrict(activityCreatedAt, { - addSuffix: true, - })} - - - {internationalTimeFormat(activityCreatedAt)} - -
-
-
-
+ + {renderActivityPrefix()} + {getDetail(activity, isPremiumTier)} + ); }; -export default ActivityItem; +export default GlobalActivityItem; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/index.ts b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/index.ts new file mode 100644 index 000000000000..5c079f685e65 --- /dev/null +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/index.ts @@ -0,0 +1 @@ +export { default } from "./GlobalActivityItem"; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 47e3184f590d..d96b3f5e60f6 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -31,6 +31,7 @@ import { IHostPolicy } from "interfaces/policy"; import { IQueryStats } from "interfaces/query_stats"; import { IHostSoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; +import { IHostUpcomingActivity } from "interfaces/activity"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; import permissions from "utilities/permissions"; @@ -57,6 +58,7 @@ import { IPackageInstallDetails, } from "components/ActivityDetails/InstallDetails/SoftwareInstallDetails/SoftwareInstallDetails"; import SoftwareUninstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal"; +import { IShowActivityDetailsData } from "components/ActivityItem/ActivityItem"; import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; @@ -81,7 +83,6 @@ import BootstrapPackageModal from "./modals/BootstrapPackageModal"; import ScriptModalGroup from "./modals/ScriptModalGroup"; import SelectQueryModal from "./modals/SelectQueryModal"; import HostDetailsBanners from "./components/HostDetailsBanners"; -import { IShowActivityDetailsData } from "../cards/Activity/Activity"; import LockModal from "./modals/LockModal"; import UnlockModal from "./modals/UnlockModal"; import { @@ -92,6 +93,7 @@ import WipeModal from "./modals/WipeModal"; import SoftwareDetailsModal from "../cards/Software/SoftwareDetailsModal"; import { parseHostSoftwareQueryParams } from "../cards/Software/HostSoftware"; import { getErrorMessage } from "./helpers"; +import CancelActivityModal from "./modals/CancelActivityModal"; const baseClass = "host-details"; @@ -194,6 +196,10 @@ const HostDetailsPage = ({ selectedSoftwareDetails, setSelectedSoftwareDetails, ] = useState(null); + const [ + selectedCancelActivity, + setSelectedCancelActivity, + ] = useState(null); // activity states const [activeActivityTab, setActiveActivityTab] = useState< @@ -695,6 +701,10 @@ const HostDetailsPage = ({ } }; + const onCancelActivity = (activity: IHostUpcomingActivity) => { + setSelectedCancelActivity(activity); + }; + const renderActionDropdown = () => { if (!host) { return null; @@ -872,6 +882,7 @@ const HostDetailsPage = ({ onNextPage={() => setActivityPage(activityPage + 1)} onPreviousPage={() => setActivityPage(activityPage - 1)} onShowDetails={onShowActivityDetails} + onCancel={onCancelActivity} /> {!isIosOrIpadosHost && ( setSelectedSoftwareDetails(null)} /> )} + {selectedCancelActivity && ( + setSelectedCancelActivity(null)} + /> + )} ); diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/CancelActivityModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/CancelActivityModal.tsx new file mode 100644 index 000000000000..cc8a6871aaff --- /dev/null +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/CancelActivityModal.tsx @@ -0,0 +1,66 @@ +import React, { useContext } from "react"; +import { noop } from "lodash"; + +import { IHostUpcomingActivity } from "interfaces/activity"; +import activitiesAPI from "services/entities/activities"; +import { NotificationContext } from "context/notification"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; + +import { upcomingActivityComponentMap } from "pages/hosts/details/cards/Activity/ActivityConfig"; + +import { getErrorMessage } from "./helpers"; + +const baseClass = "cancel-activity-modal"; + +interface ICancelActivityModalProps { + hostId: number; + activity: IHostUpcomingActivity; + onCancel: () => void; +} + +const CancelActivityModal = ({ + hostId, + activity, + onCancel, +}: ICancelActivityModalProps) => { + const { renderFlash } = useContext(NotificationContext); + + const ActivityItemComponent = upcomingActivityComponentMap[activity.type]; + + const onCancelActivity = async () => { + try { + await activitiesAPI.cancelHostActivity(hostId, activity.uuid); + renderFlash("success", "Activity successfully canceled."); + } catch (error) { + // TODO: hook up error message when API is updated + renderFlash("error", getErrorMessage(error)); + } + onCancel(); + }; + + return ( + + <> + +
+ + +
+ +
+ ); +}; + +export default CancelActivityModal; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/helpers.ts b/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/helpers.ts new file mode 100644 index 000000000000..09f2d1b39d34 --- /dev/null +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/helpers.ts @@ -0,0 +1,7 @@ +import { getErrorReason } from "interfaces/errors"; + +// eslint-disable-next-line import/prefer-default-export +export const getErrorMessage = (err: unknown) => { + const reason = getErrorReason(err); + return reason; +}; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/index.ts b/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/index.ts new file mode 100644 index 000000000000..d2492d112102 --- /dev/null +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/CancelActivityModal/index.ts @@ -0,0 +1 @@ +export { default } from "./CancelActivityModal"; diff --git a/frontend/pages/hosts/details/cards/Activity/Activity.tsx b/frontend/pages/hosts/details/cards/Activity/Activity.tsx index 507ceaa335bc..58646d7661f3 100644 --- a/frontend/pages/hosts/details/cards/Activity/Activity.tsx +++ b/frontend/pages/hosts/details/cards/Activity/Activity.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; -import { IActivityDetails } from "interfaces/activity"; +import { IHostUpcomingActivity } from "interfaces/activity"; import { IHostPastActivitiesResponse, IHostUpcomingActivitiesResponse, @@ -11,34 +11,17 @@ import Card from "components/Card"; import TabsWrapper from "components/TabsWrapper"; import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; +import { ShowActivityDetailsHandler } from "components/ActivityItem/ActivityItem"; import PastActivityFeed from "./PastActivityFeed"; import UpcomingActivityFeed from "./UpcomingActivityFeed"; const baseClass = "activity-card"; -export interface IShowActivityDetailsData { - type: string; - details?: IActivityDetails; -} - -export type ShowActivityDetailsHandler = ( - data: IShowActivityDetailsData -) => void; - const UpcomingTooltip = () => { return ( - Currently, only scripts run before other scripts and -
- software is installed before other software. -
-
- Failure of one activity won't cancel other activities. - - } + tipContent="Failure of one activity won't cancel other activities." className={`${baseClass}__upcoming-tooltip`} > Activities run as listed @@ -56,6 +39,7 @@ interface IActivityProps { onNextPage: () => void; onPreviousPage: () => void; onShowDetails: ShowActivityDetailsHandler; + onCancel: (activity: IHostUpcomingActivity) => void; } const Activity = ({ @@ -68,6 +52,7 @@ const Activity = ({ onNextPage, onPreviousPage, onShowDetails, + onCancel, }: IActivityProps) => { return ( void; } export const pastActivityComponentMap: Record< @@ -38,6 +49,8 @@ export const pastActivityComponentMap: Record< [ActivityType.InstalledSoftware]: InstalledSoftwareActivityItem, [ActivityType.UninstalledSoftware]: InstalledSoftwareActivityItem, [ActivityType.InstalledAppStoreApp]: InstalledSoftwareActivityItem, + [ActivityType.CanceledScript]: CanceledScriptActivityItem, + [ActivityType.CanceledSoftwareInstall]: CanceledSoftwareInstallActivityItem, }; export const upcomingActivityComponentMap: Record< diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/CanceledScriptActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/CanceledScriptActivityItem.tsx new file mode 100644 index 000000000000..dea1419c9121 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/CanceledScriptActivityItem.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +import { formatScriptNameForActivityItem } from "utilities/helpers"; + +import ActivityItem from "components/ActivityItem"; + +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const baseClass = "canceled-script-activity-item"; + +const CanceledScriptActivityItem = ({ + activity, +}: IHostActivityItemComponentProps) => { + return ( + + <> + {activity.actor_full_name} canceled{" "} + {formatScriptNameForActivityItem(activity.details?.script_name)}{" "} + script on this host. + + + ); +}; + +export default CanceledScriptActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/index.ts new file mode 100644 index 000000000000..b0d5e965b396 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledScriptActivityItem/index.ts @@ -0,0 +1 @@ +export { default } from "./CanceledScriptActivityItem"; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/CanceledSoftwareInstallActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/CanceledSoftwareInstallActivityItem.tsx new file mode 100644 index 000000000000..1bb132a6eee8 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/CanceledSoftwareInstallActivityItem.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import ActivityItem from "components/ActivityItem"; +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const baseClass = "canceled-software-install-activity-item"; + +const CanceledSoftwareInstallActivityItem = ({ + activity, +}: IHostActivityItemComponentProps) => { + return ( + + <> + {activity.actor_full_name} canceled{" "} + {activity.details?.software_title} install on this host. + + + ); +}; + +export default CanceledSoftwareInstallActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/index.ts new file mode 100644 index 000000000000..d769467c5aa5 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CanceledSoftwareInstallActivityItem/index.ts @@ -0,0 +1 @@ +export { default } from "./CanceledSoftwareInstallActivityItem"; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx index c3c6384944be..16cd34a441dc 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem.tsx @@ -2,15 +2,16 @@ import React from "react"; import { getInstallStatusPredicate } from "interfaces/software"; +import ActivityItem from "components/ActivityItem"; + import { IHostActivityItemComponentPropsWithShowDetails } from "../../ActivityConfig"; -import HostActivityItem from "../../HostActivityItem"; -import ShowDetailsButton from "../../ShowDetailsButton"; const baseClass = "installed-software-activity-item"; const InstalledSoftwareActivityItem = ({ activity, onShowDetails, + hideCancel, }: IHostActivityItemComponentPropsWithShowDetails) => { const { actor_full_name: actorName, details } = activity; const { self_service, software_title: title } = details; @@ -18,17 +19,21 @@ const InstalledSoftwareActivityItem = ({ details.status === "failed" ? "failed_uninstall" : details.status; const actorDisplayName = self_service ? ( - An end user + End user ) : ( {actorName} ); return ( - + <>{actorDisplayName} {getInstallStatusPredicate(status)} {title}{" "} - on this host.{" "} - - + on this host {self_service && "(self-service)"}.{" "} + ); }; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/LockedHostActivityItem/LockedHostActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/LockedHostActivityItem/LockedHostActivityItem.tsx index 09bc6a7c8902..10004775a627 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityItems/LockedHostActivityItem/LockedHostActivityItem.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/LockedHostActivityItem/LockedHostActivityItem.tsx @@ -1,7 +1,8 @@ import React from "react"; +import ActivityItem from "components/ActivityItem"; + import { IHostActivityItemComponentProps } from "../../ActivityConfig"; -import HostActivityItem from "../../HostActivityItem"; const baseClass = "locked-host-activity-item"; @@ -9,9 +10,9 @@ const LockedHostActivityItem = ({ activity, }: IHostActivityItemComponentProps) => { return ( - + {activity.actor_full_name} locked this host. - + ); }; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanScriptActivityItem/RanScriptActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanScriptActivityItem/RanScriptActivityItem.tsx index e0f26d0a8a64..19b8b97bd602 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanScriptActivityItem/RanScriptActivityItem.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanScriptActivityItem/RanScriptActivityItem.tsx @@ -2,9 +2,8 @@ import React from "react"; import { formatScriptNameForActivityItem } from "utilities/helpers"; -import HostActivityItem from "../../HostActivityItem"; +import ActivityItem from "components/ActivityItem"; import { IHostActivityItemComponentPropsWithShowDetails } from "../../ActivityConfig"; -import ShowDetailsButton from "../../ShowDetailsButton"; const baseClass = "ran-script-activity-item"; @@ -12,20 +11,29 @@ const RanScriptActivityItem = ({ tab, activity, onShowDetails, + onCancel, + isSoloActivity, + hideCancel, }: IHostActivityItemComponentPropsWithShowDetails) => { const ranScriptPrefix = tab === "past" ? "ran" : "told Fleet to run"; return ( - + {activity.actor_full_name} <> {" "} {ranScriptPrefix}{" "} {formatScriptNameForActivityItem(activity.details?.script_name)} on this host.{" "} - - + ); }; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/UnlockedHostActivityItem/UnlockedHostActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/UnlockedHostActivityItem/UnlockedHostActivityItem.tsx index 6fa7cd0550fe..c30126543b0a 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityItems/UnlockedHostActivityItem/UnlockedHostActivityItem.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/UnlockedHostActivityItem/UnlockedHostActivityItem.tsx @@ -1,7 +1,7 @@ import React from "react"; +import ActivityItem from "components/ActivityItem"; import { IHostActivityItemComponentProps } from "../../ActivityConfig"; -import HostActivityItem from "../../HostActivityItem"; const baseClass = "unlocked-host-activity-item"; @@ -13,9 +13,9 @@ const UnlockedHostActivityItem = ({ desc = "viewed the six-digit unlock PIN for this host."; } return ( - + {activity.actor_full_name} {desc} - + ); }; diff --git a/frontend/pages/hosts/details/cards/Activity/HostActivityItem/HostActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/HostActivityItem/HostActivityItem.tsx deleted file mode 100644 index 44cc1267c0c2..000000000000 --- a/frontend/pages/hosts/details/cards/Activity/HostActivityItem/HostActivityItem.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from "react"; -import ReactTooltip from "react-tooltip"; -import { formatDistanceToNowStrict } from "date-fns"; -import classnames from "classnames"; - -import { IActivity } from "interfaces/activity"; -import { - addGravatarUrlToResource, - internationalTimeFormat, -} from "utilities/helpers"; -import { DEFAULT_GRAVATAR_LINK } from "utilities/constants"; - -import Avatar from "components/Avatar"; - -import { COLORS } from "styles/var/colors"; - -const baseClass = "host-activity-item"; - -interface IHostActivityItemProps { - activity: IActivity; - children: React.ReactNode; - className?: string; -} - -/** - * A wrapper that will render all the common elements of a host activity item. - * This includes the avatar, the created at timestamp, and a dash to separate - * the activity items. The `children` will be the specific details of the activity - * implemented in the component that uses this wrapper. - */ -const HostActivityItem = ({ - activity, - children, - className, -}: IHostActivityItemProps) => { - const { actor_email } = activity; - const { gravatar_url } = actor_email - ? addGravatarUrlToResource({ email: actor_email }) - : { gravatar_url: DEFAULT_GRAVATAR_LINK }; - - // wrapped just in case the date string does not parse correctly - let activityCreatedAt: Date | null = null; - try { - activityCreatedAt = new Date(activity.created_at); - } catch (e) { - activityCreatedAt = null; - } - - const classNames = classnames(baseClass, className); - - return ( -
- -
-
- - {children} - -
- - {activityCreatedAt && - formatDistanceToNowStrict(activityCreatedAt, { - addSuffix: true, - })} - - {activityCreatedAt && ( - - {internationalTimeFormat(activityCreatedAt)} - - )} -
-
-
-
- ); -}; - -export default HostActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/HostActivityItem/_styles.scss b/frontend/pages/hosts/details/cards/Activity/HostActivityItem/_styles.scss deleted file mode 100644 index 187a8e145598..000000000000 --- a/frontend/pages/hosts/details/cards/Activity/HostActivityItem/_styles.scss +++ /dev/null @@ -1,65 +0,0 @@ -.host-activity-item { - display: grid; // Grid system is used to create variable dashed line lengths - grid-template-columns: 16px 16px 1fr; - grid-template-rows: 32px max-content; - - .avatar-wrapper { - grid-column-start: 1; - width: 32px; - height: 32px; - } - - &__dash { - border-right: 1px dashed $ui-fleet-black-25; - grid-column-start: 1; - grid-row-start: 2; - grid-row-end: 3; - } - - &__details-wrapper { - grid-column-start: 3; - grid-row-start: 1; - grid-row-end: 3; - padding-left: $pad-large; - padding-bottom: $pad-large; - - .premium-icon-tip { - position: relative; - top: 4px; - padding-right: $pad-xsmall; - } - - .activity-details { - margin: 0; - line-height: 16px; - } - } - - &__details-topline { - font-size: $x-small; - overflow-wrap: anywhere; - } - - &__details-content { - margin-right: $pad-xsmall; - } - - &__details-bottomline { - font-size: $xx-small; - color: $ui-fleet-black-50; - } - - &__show-query-icon { - margin-left: $pad-xsmall; - } - - &:last-child { - .host-activity-item__dash { - border-right: none; - } - - .host-activity-item__details-wrapper { - padding-bottom: $pad-xxlarge; - } - } -} diff --git a/frontend/pages/hosts/details/cards/Activity/HostActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/HostActivityItem/index.ts deleted file mode 100644 index b711dde7738c..000000000000 --- a/frontend/pages/hosts/details/cards/Activity/HostActivityItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./HostActivityItem"; diff --git a/frontend/pages/hosts/details/cards/Activity/PastActivityFeed/PastActivityFeed.tsx b/frontend/pages/hosts/details/cards/Activity/PastActivityFeed/PastActivityFeed.tsx index 89b038eb9581..cb88c0d35a1a 100644 --- a/frontend/pages/hosts/details/cards/Activity/PastActivityFeed/PastActivityFeed.tsx +++ b/frontend/pages/hosts/details/cards/Activity/PastActivityFeed/PastActivityFeed.tsx @@ -7,9 +7,9 @@ import { IHostPastActivitiesResponse } from "services/entities/activities"; import FleetIcon from "components/icons/FleetIcon"; import Button from "components/buttons/Button"; import DataError from "components/DataError"; +import { ShowActivityDetailsHandler } from "components/ActivityItem/ActivityItem"; import EmptyFeed from "../EmptyFeed/EmptyFeed"; -import { ShowActivityDetailsHandler } from "../Activity"; import { pastActivityComponentMap } from "../ActivityConfig"; @@ -18,7 +18,7 @@ const baseClass = "past-activity-feed"; interface IPastActivityFeedProps { activities?: IHostPastActivitiesResponse; isError?: boolean; - onDetailsClick: ShowActivityDetailsHandler; + onShowDetails: ShowActivityDetailsHandler; onNextPage: () => void; onPreviousPage: () => void; } @@ -26,7 +26,7 @@ interface IPastActivityFeedProps { const PastActivityFeed = ({ activities, isError = false, - onDetailsClick, + onShowDetails, onNextPage, onPreviousPage, }: IPastActivityFeedProps) => { @@ -55,7 +55,8 @@ const PastActivityFeed = ({
{activitiesList.map((activity: IHostPastActivity) => { // TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in - // the backend. For now, if all these fields are empty, then we assume it was Fleet-initiated. + // the backend. For now, if all these fields are empty, then we assume it was + // Fleet-initiated. if ( !activity.actor_email && !activity.actor_full_name && @@ -71,7 +72,8 @@ const PastActivityFeed = ({ key={activity.id} tab="past" activity={activity} - onShowDetails={onDetailsClick} + hideCancel + onShowDetails={onShowDetails} /> ); })} diff --git a/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/ShowDetailsButton.tsx b/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/ShowDetailsButton.tsx deleted file mode 100644 index 9a80b891b4e3..000000000000 --- a/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/ShowDetailsButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; - -import { IActivity } from "interfaces/activity"; - -import Icon from "components/Icon"; -import Button from "components/buttons/Button"; - -import { ShowActivityDetailsHandler } from "../Activity"; - -const baseClass = "show-details-button"; - -interface IShowDetailsButtonProps { - activity: IActivity; - onShowDetails: ShowActivityDetailsHandler; -} - -const ShowDetailsButton = ({ - activity, - onShowDetails, -}: IShowDetailsButtonProps) => { - return ( - - ); -}; - -export default ShowDetailsButton; diff --git a/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/_styles.scss b/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/_styles.scss deleted file mode 100644 index c1f3a99a3211..000000000000 --- a/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/_styles.scss +++ /dev/null @@ -1,5 +0,0 @@ -.show-details-button { - &__show-details-icon { - margin-left: $pad-xsmall; - } -} diff --git a/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/index.ts b/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/index.ts deleted file mode 100644 index 533c76edddb3..000000000000 --- a/frontend/pages/hosts/details/cards/Activity/ShowDetailsButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ShowDetailsButton"; diff --git a/frontend/pages/hosts/details/cards/Activity/UpcomingActivityFeed/UpcomingActivityFeed.tsx b/frontend/pages/hosts/details/cards/Activity/UpcomingActivityFeed/UpcomingActivityFeed.tsx index 320cd632ec02..686e0da5dc4c 100644 --- a/frontend/pages/hosts/details/cards/Activity/UpcomingActivityFeed/UpcomingActivityFeed.tsx +++ b/frontend/pages/hosts/details/cards/Activity/UpcomingActivityFeed/UpcomingActivityFeed.tsx @@ -7,9 +7,9 @@ import { IHostUpcomingActivitiesResponse } from "services/entities/activities"; import FleetIcon from "components/icons/FleetIcon"; import DataError from "components/DataError"; import Button from "components/buttons/Button"; +import { ShowActivityDetailsHandler } from "components/ActivityItem/ActivityItem"; import EmptyFeed from "../EmptyFeed/EmptyFeed"; -import { ShowActivityDetailsHandler } from "../Activity"; import { upcomingActivityComponentMap } from "../ActivityConfig"; const baseClass = "upcoming-activity-feed"; @@ -17,7 +17,8 @@ const baseClass = "upcoming-activity-feed"; interface IUpcomingActivityFeedProps { activities?: IHostUpcomingActivitiesResponse; isError?: boolean; - onDetailsClick: ShowActivityDetailsHandler; + onShowDetails: ShowActivityDetailsHandler; + onCancel: (activity: IHostUpcomingActivity) => void; onNextPage: () => void; onPreviousPage: () => void; } @@ -25,7 +26,8 @@ interface IUpcomingActivityFeedProps { const UpcomingActivityFeed = ({ activities, isError = false, - onDetailsClick, + onShowDetails, + onCancel, onNextPage, onPreviousPage, }: IUpcomingActivityFeedProps) => { @@ -51,7 +53,7 @@ const UpcomingActivityFeed = ({ return (
-
+
{activitiesList.map((activity: IHostUpcomingActivity) => { // TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in // the backend. For now, if all these fields are empty, then we assume it was @@ -72,7 +74,9 @@ const UpcomingActivityFeed = ({ key={activity.id} tab="upcoming" activity={activity} - onShowDetails={onDetailsClick} + onShowDetails={onShowDetails} + hideCancel // TODO: remove this when canceling is implemented in API + onCancel={() => onCancel(activity)} /> ); })} diff --git a/frontend/services/entities/activities.ts b/frontend/services/entities/activities.ts index 3aa2a263c564..24fdd2a20ef4 100644 --- a/frontend/services/entities/activities.ts +++ b/frontend/services/entities/activities.ts @@ -95,4 +95,9 @@ export default { return sendRequest("GET", path); }, + + cancelHostActivity: (hostId: number, uuid: string) => { + const { HOST_CANCEL_ACTIVITY } = endpoints; + return sendRequest("DELETE", HOST_CANCEL_ACTIVITY(hostId, uuid)); + }, }; diff --git a/frontend/utilities/date_format/date_format.tests.ts b/frontend/utilities/date_format/date_format.tests.ts index b39d0d1eebbe..902d3f19e895 100644 --- a/frontend/utilities/date_format/date_format.tests.ts +++ b/frontend/utilities/date_format/date_format.tests.ts @@ -1,4 +1,4 @@ -import { monthDayYearFormat, uploadedFromNow } from "."; +import { dateAgo, monthDayYearFormat, uploadedFromNow } from "."; describe("date_format utilities", () => { describe("uploadedFromNow util", () => { @@ -17,4 +17,21 @@ describe("date_format utilities", () => { expect(monthDayYearFormat(date)).toEqual("November 29, 2024"); }); }); + + describe("dateAgo util", () => { + it("returns a user friendly date ago message from a date string", () => { + const currentDate = new Date(); + currentDate.setDate(currentDate.getDate() - 2); + const twoDaysAgo = currentDate.toISOString(); + + expect(dateAgo(twoDaysAgo)).toEqual("2 days ago"); + }); + + it("returns a user friendly date ago message from a Date object", () => { + const date = new Date(); + date.setDate(date.getDate() - 2); + + expect(dateAgo(date)).toEqual("2 days ago"); + }); + }); }); diff --git a/frontend/utilities/date_format/index.ts b/frontend/utilities/date_format/index.ts index 538c3a827da7..e05f61229076 100644 --- a/frontend/utilities/date_format/index.ts +++ b/frontend/utilities/date_format/index.ts @@ -1,19 +1,20 @@ -import { format, formatDistanceToNow, intlFormat } from "date-fns"; +import { format, formatDistanceToNow } from "date-fns"; /** Utility to create a string from a date in this format: `Uploaded .... ago` */ export const uploadedFromNow = (date: string) => { // NOTE: Malformed dates will result in errors. This is expected "fail loudly" behavior. - return `Uploaded ${formatDistanceToNow(new Date(date))} ago`; + return `Uploaded ${formatDistanceToNow(new Date(date), { addSuffix: true })}`; }; /** Utility to create a string from a date in this format: `.... ago` */ -export const dateAgo = (date: string) => { +export const dateAgo = (date: string | Date) => { // NOTE: Malformed dates will result in errors. This is expected "fail loudly" behavior. - return `${formatDistanceToNow(new Date(date))} ago`; + date = date instanceof Date ? date : new Date(date); + return `${formatDistanceToNow(date, { addSuffix: true })}`; }; /** diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 89845b9f7841..4fa098ef1b49 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -9,6 +9,9 @@ export default { HOST_UPCOMING_ACTIVITIES: (id: number): string => { return `/${API_VERSION}/fleet/hosts/${id}/activities/upcoming`; }, + HOST_CANCEL_ACTIVITY: (hostId: number, uuid: string): string => { + return `/${API_VERSION}/fleet/hosts/${hostId}/activities/upcoming/${uuid}`; + }, CHANGE_PASSWORD: `/${API_VERSION}/fleet/change_password`, CONFIG: `/${API_VERSION}/fleet/config`,