Fleet UI: Update Android status tooltips + global activities (#37185)

This commit is contained in:
RachelElysia 2025-12-15 09:28:55 -05:00 committed by GitHub
parent d8a3af4e88
commit 229481fc79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 225 additions and 46 deletions

View file

@ -270,7 +270,7 @@ export interface IActivityDetails {
}
export const ACTIVITY_DISPLAY_NAME_MAP: Record<ActivityType, string> = {
added_app_store_app: "Added App Store (VPP) app",
added_app_store_app: "Added App Store app", // Includes VPP and Android Playstore apps
added_bootstrap_package: "Added bootstrap package",
added_conditional_access_microsoft: "Added conditional access: Microsoft",
added_custom_scep_proxy: "Added certificate authority (CA): custom SCEP",
@ -299,7 +299,7 @@ export const ACTIVITY_DISPLAY_NAME_MAP: Record<ActivityType, string> = {
created_team: "Added team",
created_user: "Added user",
created_windows_profile: "Added configuration profile: Windows",
deleted_app_store_app: "Deleted App Store (VPP) app",
deleted_app_store_app: "Deleted App Store app", // Includes VPP and Android Playstore apps
deleted_bootstrap_package: "Deleted bootstrap package",
deleted_conditional_access_microsoft: "Deleted conditional access: Microsoft",
deleted_custom_scep_proxy: "Deleted certificate authority (CA): custom SCEP",
@ -334,7 +334,7 @@ export const ACTIVITY_DISPLAY_NAME_MAP: Record<ActivityType, string> = {
disabled_windows_mdm_migration: "Turned off Windows MDM migration",
edited_activity_automations: "Edited activity automations",
edited_agent_options: "Edited agent options",
edited_app_store_app: "Edited App Store (VPP) app",
edited_app_store_app: "Edited App Store app", // Includes VPP and Android Playstore apps
edited_conditional_access_microsoft: "Edited conditional access: Microsoft",
edited_custom_scep_proxy: "Edited certificate authority (CA): custom SCEP",
edited_declaration_profile: "GitOps: edited declaration (DDM) profiles",

View file

@ -7,11 +7,11 @@
flex: 1;
min-width: 0; // allows the dropdowns to shrink properly in flex container
}
&__dropdown-filters {
display: grid;
grid-template-areas: "type date sort";
grid-template-columns: repeat(3, 1fr);
grid-template-columns: 4fr 3fr 3fr;
gap: $pad-medium;
}
@ -30,8 +30,8 @@
// this container is defined in the dashboard page .scss file
@container activity-feed-card (width < 436px) {
.activity-feed-filters__dropdown-filters {
grid-template-columns: repeat(2, 1fr);
grid-template-areas:
grid-template-columns: repeat(2, 1fr);
grid-template-areas:
"type type"
"date sort";

View file

@ -1,6 +1,9 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, within, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import { getPathWithQueryParams } from "utilities/url";
import PATHS from "router/paths";
import InstallerStatusTable from "./InstallerStatusTable";
describe("InstallerStatusTable", () => {
@ -17,13 +20,168 @@ describe("InstallerStatusTable", () => {
// Check cell values (always "hosts", even for 1)
const cells = screen.getAllByRole("cell");
expect(cells[0]).toHaveTextContent("0 hosts");
expect(cells[1]).toHaveTextContent("1 host");
expect(cells[2]).toHaveTextContent("3 hosts");
// Check the anchor and its text in each cell
expect(cells[0].querySelector("a.link-cell")).toHaveTextContent("0 hosts");
expect(cells[1].querySelector("a.link-cell")).toHaveTextContent("1 host");
expect(cells[2].querySelector("a.link-cell")).toHaveTextContent("3 hosts");
const installedLink = cells[0].querySelector("a.link-cell");
const pendingLink = cells[1].querySelector("a.link-cell");
const failedLink = cells[2].querySelector("a.link-cell");
expect(installedLink).toHaveTextContent("0 hosts");
expect(pendingLink).toHaveTextContent("1 host");
expect(failedLink).toHaveTextContent("3 hosts");
});
it("renders correct header titles for install vs script package", () => {
const { rerender } = render(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isScriptPackage={false}
/>
);
let headers = screen.getAllByRole("columnheader");
expect(headers[0]).toHaveTextContent("Installed");
expect(headers[1]).toHaveTextContent("Pending");
expect(headers[2]).toHaveTextContent("Failed");
rerender(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isScriptPackage
/>
);
headers = screen.getAllByRole("columnheader");
expect(headers[0]).toHaveTextContent("Ran");
expect(headers[1]).toHaveTextContent("Pending");
expect(headers[2]).toHaveTextContent("Failed");
});
it("renders different tooltips for Android Play Store vs non-Android for pending", async () => {
// non-Android: pending install/uninstall message
const { user, rerender } = render(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isAndroidPlayStoreApp={false}
/>
);
let pendingHeader = screen.getByText(/pending/i);
await user.hover(pendingHeader);
await waitFor(() => {
expect(
screen.getByText(/Fleet is installing\/uninstalling or will/i)
).toBeInTheDocument();
});
// Android: Play Storestyle message
rerender(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isAndroidPlayStoreApp
/>
);
pendingHeader = screen.getByText(/pending/i);
await user.hover(pendingHeader);
await waitFor(() => {
expect(
screen.getByText(/Software will be installed or configuration will/i)
).toBeInTheDocument();
});
});
it("hides installed tooltip for Android Play Store app", async () => {
const { user, rerender } = render(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isAndroidPlayStoreApp={false}
/>
);
let installedHeader = screen.getByText(/installed/i);
await user.hover(installedHeader);
await waitFor(() => {
expect(
screen.getByText(/Software is installed on these hosts/i)
).toBeInTheDocument();
});
rerender(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isAndroidPlayStoreApp
/>
);
installedHeader = screen.getByText(/installed/i);
await user.hover(installedHeader);
// Installed tooltip returns null for Android Play Store
await waitFor(() => {
expect(
screen.queryByText(/Software is installed on these hosts/i)
).not.toBeInTheDocument();
});
});
it("renders failed tooltip text correctly for Android vs non-Android", async () => {
const { user, rerender } = render(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isAndroidPlayStoreApp={false}
/>
);
let failedHeader = screen.getByText(/failed/i);
await user.hover(failedHeader);
await waitFor(() => {
expect(
screen.getByText(/These hosts failed to install\/uninstall software/i)
).toBeInTheDocument();
});
rerender(
<InstallerStatusTable
softwareId={1}
teamId={1}
status={{ installed: 0, pending: 0, failed: 0 }}
isAndroidPlayStoreApp
/>
);
failedHeader = screen.getByText(/failed/i);
await user.hover(failedHeader);
await waitFor(() => {
expect(
screen.getByText(
/Software failed to install or configuration failed to apply/i
)
).toBeInTheDocument();
});
});
});

View file

@ -17,6 +17,7 @@ interface IInstallerStatusTableProps {
status: ISoftwarePackageStatus | ISoftwareAppStoreAppStatus;
isLoading?: boolean;
isScriptPackage?: boolean;
isAndroidPlayStoreApp?: boolean;
}
const InstallerStatusTable = ({
@ -26,6 +27,7 @@ const InstallerStatusTable = ({
status,
isLoading = false,
isScriptPackage = false,
isAndroidPlayStoreApp = false,
}: IInstallerStatusTableProps) => {
const classNames = classnames(baseClass, className);
@ -34,6 +36,7 @@ const InstallerStatusTable = ({
softwareId,
teamId,
isScriptPackage,
isAndroidPlayStoreApp,
});
return (

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { ReactNode } from "react";
import { ISoftwareTitleVersion } from "interfaces/software";
import PATHS from "router/paths";
@ -9,12 +9,14 @@ import LinkCell from "components/TableContainer/DataTable/LinkCell";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import { isAndroid } from "interfaces/platform";
interface ISoftwareTitleDetailsTableConfigProps {
softwareId?: number;
teamId?: number;
baseClass?: string;
isScriptPackage?: boolean;
isAndroidPlayStoreApp?: boolean;
}
interface ICellProps {
cell: {
@ -28,7 +30,7 @@ interface ICellProps {
interface IStatusDisplayOption {
displayName: string;
iconName: "success" | "pending-outline" | "error";
tooltip: React.ReactNode;
tooltip: (isAndroidPlayStoreApp?: boolean) => React.ReactNode;
}
// "pending" and "failed" each encompass both "_install" and "_uninstall" sub-statuses
@ -45,42 +47,56 @@ const STATUS_DISPLAY_OPTIONS: Record<
installed: {
displayName: "Installed",
iconName: "success",
tooltip: (
<>
Software is installed on these hosts (install script finished
<br />
with exit code 0). Currently, if the software is uninstalled, the
<br />
&quot;Installed&quot; status won&apos;t be updated.
</>
),
tooltip: (isAndroidPlayStoreApp) => {
return isAndroidPlayStoreApp ? null : (
<>
Software is installed on these hosts (install script finished
<br />
with exit code 0). Currently, if the software is uninstalled, the
<br />
&quot;Installed&quot; status won&apos;t be updated.
</>
);
},
},
pending: {
displayName: "Pending",
iconName: "pending-outline",
tooltip: (
<>
Fleet is installing/uninstalling or will
<br />
do so when the host comes online.
</>
),
tooltip: (isAndroidPlayStoreApp) => {
return isAndroidPlayStoreApp ? (
<>
Software will be installed or configuration will
<br />
be applied the next time the host checks in.
</>
) : (
<>
Fleet is installing/uninstalling or will
<br />
do so when the host comes online.
</>
);
},
},
failed: {
displayName: "Failed",
iconName: "error",
tooltip: (
<>
These hosts failed to install/uninstall software.
<br />
Click on a host to view error(s).
</>
),
tooltip: (isAndroidPlayStoreApp) => {
return isAndroidPlayStoreApp ? (
<>Software failed to install or configuration failed to apply.</>
) : (
<>
These hosts failed to install/uninstall software.
<br />
Click on a host to view error(s).
</>
);
},
},
ran_script: {
displayName: "Ran",
iconName: "success",
tooltip: (
tooltip: () => (
<>
The script successfully
<br />
@ -91,7 +107,7 @@ const STATUS_DISPLAY_OPTIONS: Record<
pending_script: {
displayName: "Pending",
iconName: "pending-outline",
tooltip: (
tooltip: () => (
<>
Fleet is running the script or will do so
<br />
@ -102,7 +118,7 @@ const STATUS_DISPLAY_OPTIONS: Record<
failed_script: {
displayName: "Failed",
iconName: "error",
tooltip: (
tooltip: () => (
<>
These hosts failed to run the script.
<br />
@ -117,6 +133,7 @@ const generateSoftwareTitleDetailsTableConfig = ({
teamId,
baseClass,
isScriptPackage,
isAndroidPlayStoreApp,
}: ISoftwareTitleDetailsTableConfigProps) => {
const tableHeaders = [
{
@ -130,7 +147,7 @@ const generateSoftwareTitleDetailsTableConfig = ({
const titleWithTooltip = (
<TooltipWrapper
position="top"
tipContent={displayData.tooltip}
tipContent={displayData.tooltip(isAndroidPlayStoreApp)}
underline={false}
showArrow
tipOffset={10}
@ -167,7 +184,7 @@ const generateSoftwareTitleDetailsTableConfig = ({
return (
<TooltipWrapper
position="top"
tipContent={displayData.tooltip}
tipContent={displayData.tooltip(isAndroidPlayStoreApp)}
underline={false}
showArrow
tipOffset={10}
@ -203,7 +220,7 @@ const generateSoftwareTitleDetailsTableConfig = ({
return (
<TooltipWrapper
position="top"
tipContent={displayData.tooltip}
tipContent={displayData.tooltip(isAndroidPlayStoreApp)}
underline={false}
showArrow
tipOffset={10}

View file

@ -320,6 +320,7 @@ const SoftwareInstallerCard = ({
<div className={`${baseClass}__installer-status-table`}>
<InstallerStatusTable
isScriptPackage={isScriptPackage}
isAndroidPlayStoreApp={isAndroidPlayStoreApp}
softwareId={softwareId}
teamId={teamId}
status={status}