From 8df6ea1fd0b2cfd3ddfc8ec26af989d2f4009648 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 13 May 2025 13:41:44 -0400 Subject: [PATCH] Fleet UI: Add policies table to the sw title details page (#28886) --- .../SoftwareNameCell/SoftwareNameCell.tsx | 6 +- frontend/interfaces/software.ts | 29 +-- .../AutomaticInstallModal.tsx | 98 --------- .../AutomaticInstallModal/_styles.scss | 23 --- .../AutomaticInstallModal/index.ts | 1 - .../InstallerDetailsWidget.tests.tsx | 82 ++++++++ .../InstallerDetailsWidget.tsx} | 54 +++-- .../_styles.scss | 2 +- .../InstallerDetailsWidget/index.ts | 1 + .../InstallerPoliciesTable.tests.tsx | 25 +++ .../InstallerPoliciesTable.tsx | 64 ++++++ .../InstallerPoliciesTableConfig.tsx | 56 +++++ .../InstallerPoliciesTable/_styles.scss | 7 + .../InstallerPoliciesTable/index.ts | 1 + .../InstallerStatusTable.tests.tsx | 29 +++ .../InstallerStatusTable.tsx | 52 +++++ .../InstallerStatusTableConfig.tsx | 191 +++++++++++++++++ .../InstallerStatusTable/_styles.scss | 7 + .../InstallerStatusTable/index.ts | 1 + .../SoftwareDetailsWidget/index.ts | 1 - .../SoftwareInstallerCard.tsx | 192 +++++++----------- .../SoftwareInstallerCard/_styles.scss | 9 + .../SoftwareInstallerCard/helpers.ts | 29 ++- .../SoftwareSummaryCard.tsx | 68 +++++++ .../TitleVersionsTable.tests.tsx | 76 +++++++ .../TitleVersionsTable.tsx} | 73 +++---- .../TitleVersionsTableConfig.tsx} | 13 +- .../TitleVersionsTable/_styles.scss | 5 + .../TitleVersionsTable/index.ts | 1 + .../SoftwareSummaryCard/_styles.scss | 2 + .../SoftwareSummaryCard/index.ts | 1 + .../SoftwareTitleDetailsPage.tsx | 61 ++---- .../SoftwareTitleDetailsTable/index.ts | 1 - .../SoftwareTitleDetailsPage/_styles.scss | 6 + .../SoftwareDetailsSummary.tsx | 2 +- .../cards/SoftwareDetailsSummary/_styles.scss | 5 - 36 files changed, 895 insertions(+), 379 deletions(-) delete mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/AutomaticInstallModal.tsx delete mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/_styles.scss delete mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/index.ts create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx rename frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/{SoftwareDetailsWidget/SoftwareDetailsWidget.tsx => InstallerDetailsWidget/InstallerDetailsWidget.tsx} (61%) rename frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/{SoftwareDetailsWidget => InstallerDetailsWidget}/_styles.scss (91%) create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/index.ts create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tests.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTableConfig.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/_styles.scss create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/index.ts create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts delete mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/index.ts create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tests.tsx rename frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/{SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx => SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tsx} (64%) rename frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/{SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx => SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTableConfig.tsx} (89%) create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/_styles.scss create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/index.ts create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/_styles.scss create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/index.ts delete mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx index 668d1a7252..8dba17e893 100644 --- a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx +++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx @@ -1,8 +1,8 @@ import React from "react"; import { InjectedRouter } from "react-router"; + import ReactTooltip from "react-tooltip"; import { uniqueId } from "lodash"; - import { SELF_SERVICE_TOOLTIP } from "pages/SoftwarePage/helpers"; import Icon from "components/Icon"; @@ -49,7 +49,8 @@ const installIconMap: Record = { {count === 1 ? "A policy triggers install." : `${count} policies trigger install.`}{" "} - End users can reinstall from Fleet Desktop {">"} Self-service. +
End users can reinstall from +
Fleet Desktop {">"} Self-service. ), }, @@ -74,6 +75,7 @@ const InstallIconWithTooltip = ({ } const tooltipId = uniqueId(); + return (
{ - return ( -
  • - - {policy.name} - -
  • - ); -}; - -interface IPoliciesListProps { - teamId: number; - policies: ISoftwareInstallPolicy[]; -} - -const PoliciesList = ({ teamId, policies }: IPoliciesListProps) => { - return ( -
      - {policies.map((policy) => ( - - ))} -
    - ); -}; - -interface IAutomaticInstallModalProps { - teamId: number; - policies: ISoftwareInstallPolicy[]; - onExit: () => void; -} - -const AutomaticInstallModal = ({ - teamId, - policies, - onExit, -}: IAutomaticInstallModalProps) => { - const description = - policies.length > 1 ? ( - <> - Software will be installed when hosts fail any of these policies.{" "} - - - ) : ( - <> - Software will be installed when hosts fail this policy.{" "} - - - ); - - return ( - - <> -

    {description}

    - -
    - -
    - -
    - ); -}; - -export default AutomaticInstallModal; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/_styles.scss deleted file mode 100644 index 40aea908a2..0000000000 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/_styles.scss +++ /dev/null @@ -1,23 +0,0 @@ -.automatic-install-modal { - - &__description { - margin: 0 0 $pad-large - } - - &__list { - list-style: none; - margin: 0; - padding: 0; - border: 1px solid $ui-fleet-black-10; - border-radius: $border-radius-medium; - } - - &__list-item { - border-bottom: 1px solid $ui-fleet-black-10; - padding: $pad-small $pad-large; - - &:last-child { - border-bottom: 0; - } - } -} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/index.ts deleted file mode 100644 index adb3cad5bf..0000000000 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AutomaticInstallModal"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx new file mode 100644 index 0000000000..8705e6bc43 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import InstallerDetailsWidget from "./InstallerDetailsWidget"; + +// Mock current time for time stamp test +beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2024-05-08T10:00:00Z")); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +describe("InstallerDetailsWidget", () => { + const defaultProps = { + softwareName: "Test Software", + installerType: "package" as const, + addedTimestamp: "2024-05-06T10:00:00Z", + versionInfo: v1.2.3, + isFma: false, + }; + + it("renders the package icon when installerType is 'package'", () => { + render(); + expect(screen.queryByTestId("file-pkg-graphic")).toBeInTheDocument(); + expect(screen.queryByTestId("software-icon")).not.toBeInTheDocument(); + }); + + it("renders the software name", () => { + render(); + expect(screen.getByText("Test Software")).toBeInTheDocument(); + }); + + it("renders version info and relative time when addedTimestamp is present", () => { + render(); + expect(screen.getByText("v1.2.3")).toBeInTheDocument(); + expect(screen.getByText(/2 days ago/i)).toBeInTheDocument(); + }); + + it("renders only version info when addedTimestamp is not present", () => { + render( + + ); + expect(screen.queryByText(/2 days ago/i)).not.toBeInTheDocument(); + }); + + it("applies additional className if provided", () => { + render( + + ); + const rootDiv = document.querySelector( + ".installer-details-widget.extra-class" + ); + expect(rootDiv).toBeInTheDocument(); + }); + + it("renders custom package label", () => { + render(); + + expect(screen.getByText(/custom package/i)).toBeInTheDocument(); + }); + + it("renders FMA label", () => { + render(); + + expect(screen.getByText(/Fleet-maintained/i)).toBeInTheDocument(); + }); + + it("renders VPP label", () => { + render(); + + expect(screen.getByText(/App Store \(VPP\)/i)).toBeInTheDocument(); + }); + + it("InstallerName disables tooltip if not truncated", () => { + // useCheckTruncatedElement is mocked to return false + render(); + // TooltipWrapper is mocked, so we just check that the child is rendered + expect(screen.getByText("Test Software")).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/SoftwareDetailsWidget.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx similarity index 61% rename from frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/SoftwareDetailsWidget.tsx rename to frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx index dd986399e7..47bc0ed5cd 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/SoftwareDetailsWidget.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx @@ -12,13 +12,13 @@ import Graphic from "components/Graphic"; import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; import TooltipWrapper from "components/TooltipWrapper"; -const baseClass = "software-details-widget"; +const baseClass = "installer-details-widget"; -interface ISoftwareNameProps { +interface IInstallerNameProps { name: string; } -const SoftwareName = ({ name }: ISoftwareNameProps) => { +const InstallerName = ({ name }: IInstallerNameProps) => { const titleRef = React.useRef(null); const isTruncated = useCheckTruncatedElement(titleRef); @@ -37,21 +37,30 @@ const SoftwareName = ({ name }: ISoftwareNameProps) => { ); }; -interface ISoftwareDetailsWidget { +const renderInstallerDisplayText = (installerType: string, isFma: boolean) => { + if (installerType === "package") { + return isFma ? "Fleet-maintained" : "Custom package"; + } + return "App Store (VPP)"; +}; + +interface IInstallerDetailsWidgetProps { className?: string; softwareName: string; installerType: "package" | "vpp"; addedTimestamp?: string; versionInfo?: JSX.Element; + isFma: boolean; } -const SoftwareDetailsWidget = ({ +const InstallerDetailsWidget = ({ className, softwareName, installerType, addedTimestamp, versionInfo, -}: ISoftwareDetailsWidget) => { + isFma, +}: IInstallerDetailsWidgetProps) => { const classNames = classnames(baseClass, className); const renderIcon = () => { @@ -63,17 +72,26 @@ const SoftwareDetailsWidget = ({ }; const renderDetails = () => { - return !addedTimestamp ? ( - versionInfo - ) : ( + const renderTimeStamp = () => + addedTimestamp ? ( + <> + {" "} + •{" "} + + {addedFromNow(addedTimestamp)} + + + ) : ( + "" + ); + + return ( <> - {versionInfo} •{" "} - - {addedFromNow(addedTimestamp)} - + {renderInstallerDisplayText(installerType, isFma)} • {versionInfo} + {renderTimeStamp()} ); }; @@ -82,11 +100,11 @@ const SoftwareDetailsWidget = ({
    {renderIcon()}
    - + {renderDetails()}
    ); }; -export default SoftwareDetailsWidget; +export default InstallerDetailsWidget; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/_styles.scss similarity index 91% rename from frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/_styles.scss rename to frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/_styles.scss index 9f79664fe5..8ef4cd92cb 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/_styles.scss @@ -1,4 +1,4 @@ -.software-details-widget { +.installer-details-widget { display: flex; gap: $pad-medium; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/index.ts new file mode 100644 index 0000000000..5153bab4c6 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/index.ts @@ -0,0 +1 @@ +export { default } from "./InstallerDetailsWidget"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tests.tsx new file mode 100644 index 0000000000..4b21387081 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tests.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { screen, render } from "@testing-library/react"; +import InstallerPoliciesTable from "./InstallerPoliciesTable"; + +describe("InstallerPoliciesTable", () => { + it("renders policy names as links and footer info", () => { + const policies = [{ id: 1, name: "No Gatekeeper" }]; + + render(); + + // There should be two cells, each with a link + const cells = screen.getAllByRole("cell"); + expect(cells).toHaveLength(1); + + // Each cell should contain a link with the policy name + expect(cells[0].querySelector("a.link-cell")).toHaveTextContent( + /No Gatekeeper/i + ); + const POLICY_COUNT = /1 policy/i; + expect(screen.getByText(POLICY_COUNT)).toBeInTheDocument(); + + const FOOTER_TEXT = /Software will be installed when hosts fail/i; + expect(screen.getByText(FOOTER_TEXT)).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tsx new file mode 100644 index 0000000000..7465ef814a --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tsx @@ -0,0 +1,64 @@ +import React, { useCallback } from "react"; +import classnames from "classnames"; + +import { ISoftwareInstallPolicy } from "interfaces/software"; + +import TableContainer from "components/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; +import CustomLink from "components/CustomLink"; +import generateInstallerPoliciesTableConfig from "./InstallerPoliciesTableConfig"; + +const baseClass = "installer-policies-table"; + +interface IInstallerPoliciesTable { + className?: string; + teamId?: number; + isLoading?: boolean; + policies?: ISoftwareInstallPolicy[] | null; +} +const InstallerPoliciesTable = ({ + className, + teamId, + isLoading = false, + policies, +}: IInstallerPoliciesTable) => { + const classNames = classnames(baseClass, className); + + const softwareStatusHeaders = generateInstallerPoliciesTableConfig({ + teamId, + }); + + const renderInstallerPoliciesCount = useCallback(() => { + return ; + }, [policies?.length]); + + const renderTableHelpText = () => ( +
    + Software will be installed when hosts fail{" "} + {policies?.length === 1 ? "this policy" : "any of these policies"}.{" "} + +
    + ); + + return ( + <>} + showMarkAllPages={false} + isAllPagesSelected={false} + renderTableHelpText={renderTableHelpText} + /> + ); +}; + +export default InstallerPoliciesTable; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTableConfig.tsx new file mode 100644 index 0000000000..694a81f50f --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTableConfig.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +import { ISoftwareInstallPolicy } from "interfaces/software"; +import PATHS from "router/paths"; +import { getPathWithQueryParams } from "utilities/url"; + +import LinkCell from "components/TableContainer/DataTable/LinkCell"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; + +interface IInstallerPoliciesTableConfig { + teamId?: number; +} +interface ICellProps { + cell: { + value: string; + }; + row: { + original: ISoftwareInstallPolicy; + }; + column: { + isSortedDesc: boolean; + title: string; + }; +} + +const generateInstallerPoliciesTableConfig = ({ + teamId, +}: IInstallerPoliciesTableConfig) => { + const tableHeaders = [ + { + accessor: "name", + title: "Name", + Header: (cellProps: ICellProps) => ( + + ), + Cell: (cellProps: ICellProps) => ( + + ), + }, + ]; + + return tableHeaders; +}; + +export default generateInstallerPoliciesTableConfig; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/_styles.scss new file mode 100644 index 0000000000..bfd69d7538 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/_styles.scss @@ -0,0 +1,7 @@ +.installer-status-table { + &__status-title { + display: flex; + flex-direction: row; + gap: $pad-small; + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/index.ts new file mode 100644 index 0000000000..87d18ccfb7 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/index.ts @@ -0,0 +1 @@ +export { default } from "./InstallerPoliciesTable"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx new file mode 100644 index 0000000000..8cb6951af3 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { screen } from "@testing-library/react"; +import { createCustomRenderer } from "test/test-utils"; +import InstallerStatusTable from "./InstallerStatusTable"; + +describe("InstallerStatusTable", () => { + const render = createCustomRenderer(); + + it("renders columns and links for statuses", () => { + render( + + ); + + // 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"); + }); +}); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx new file mode 100644 index 0000000000..e01118e469 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import classnames from "classnames"; + +import TableContainer from "components/TableContainer"; +import { + ISoftwarePackageStatus, + ISoftwareAppStoreAppStatus, +} from "interfaces/software"; +import generateSoftwareTitleDetailsTableConfig from "./InstallerStatusTableConfig"; + +const baseClass = "installer-status-table"; + +interface IInstallerStatusTableProps { + className?: string; + softwareId: number; + teamId?: number; + status: ISoftwarePackageStatus | ISoftwareAppStoreAppStatus; + isLoading?: boolean; +} +const InstallerStatusTable = ({ + className, + softwareId, + teamId, + status, + isLoading = false, +}: IInstallerStatusTableProps) => { + const classNames = classnames(baseClass, className); + + const softwareStatusHeaders = generateSoftwareTitleDetailsTableConfig({ + baseClass: classNames, + softwareId, + teamId, + }); + + return ( + <>} + showMarkAllPages={false} + isAllPagesSelected={false} + disableHighlightOnHover + hideFooter + /> + ); +}; + +export default InstallerStatusTable; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx new file mode 100644 index 0000000000..022ea46b96 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx @@ -0,0 +1,191 @@ +import React from "react"; + +import { ISoftwareTitleVersion } from "interfaces/software"; +import PATHS from "router/paths"; +import { getPathWithQueryParams } from "utilities/url"; +import { generateResultsCountText } from "components/TableContainer/utilities/TableContainerUtils"; + +import LinkCell from "components/TableContainer/DataTable/LinkCell"; +import TooltipWrapper from "components/TooltipWrapper"; +import Icon from "components/Icon"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; + +interface ISoftwareTitleDetailsTableConfigProps { + softwareId?: number; + teamId?: number; + baseClass?: string; +} +interface ICellProps { + cell: { + value: number; + }; + row: { + original: ISoftwareTitleVersion; + }; +} + +interface IStatusDisplayOption { + displayName: string; + iconName: "success" | "pending-outline" | "error"; + tooltip: React.ReactNode; +} + +// "pending" and "failed" each encompass both "_install" and "_uninstall" sub-statuses +type SoftwareInstallDisplayStatus = "installed" | "pending" | "failed"; + +const STATUS_DISPLAY_OPTIONS: Record< + SoftwareInstallDisplayStatus, + IStatusDisplayOption +> = { + installed: { + displayName: "Installed", + iconName: "success", + tooltip: ( + <> + Software is installed on these hosts (install script finished +
    + with exit code 0). Currently, if the software is uninstalled, the +
    + "Installed" status won't be updated. + + ), + }, + pending: { + displayName: "Pending", + iconName: "pending-outline", + tooltip: ( + <> + Fleet is installing/uninstalling or will +
    + do so when the host comes online. + + ), + }, + failed: { + displayName: "Failed", + iconName: "error", + tooltip: ( + <> + These hosts failed to install/uninstall software. +
    + Click on a host to view error(s). + + ), + }, +}; + +const generateSoftwareTitleDetailsTableConfig = ({ + softwareId, + teamId, + baseClass, +}: ISoftwareTitleDetailsTableConfigProps) => { + const tableHeaders = [ + { + accessor: "installed", + disableSortBy: true, + title: "Installed", + Header: () => { + const displayData = STATUS_DISPLAY_OPTIONS.installed; + const titleWithTooltip = ( + +
    + +
    {displayData.displayName}
    +
    +
    + ); + return ; + }, + Cell: (cellProps: ICellProps) => { + return ( + + ); + }, + }, + { + accessor: "pending", + disableSortBy: true, + title: "Pending", + Header: () => { + const displayData = STATUS_DISPLAY_OPTIONS.pending; + return ( + +
    + +
    {displayData.displayName}
    +
    +
    + ); + }, + Cell: (cellProps: ICellProps) => { + return ( + + ); + }, + }, + { + accessor: "failed", + disableSortBy: true, + title: "Failed", + Header: () => { + const displayData = STATUS_DISPLAY_OPTIONS.failed; + return ( + +
    + +
    {displayData.displayName}
    +
    +
    + ); + }, + Cell: (cellProps: ICellProps) => { + return ( + + ); + }, + }, + ]; + + return tableHeaders; +}; + +export default generateSoftwareTitleDetailsTableConfig; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss new file mode 100644 index 0000000000..bfd69d7538 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss @@ -0,0 +1,7 @@ +.installer-status-table { + &__status-title { + display: flex; + flex-direction: row; + gap: $pad-small; + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts new file mode 100644 index 0000000000..7c332f084a --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts @@ -0,0 +1 @@ +export { default } from "./InstallerStatusTable"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/index.ts deleted file mode 100644 index cf429ac917..0000000000 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SoftwareDetailsWidget"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx index 7762f7c898..ef34a8aeec 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useContext, useState } from "react"; -import PATHS from "router/paths"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; import { @@ -12,33 +11,32 @@ import { } from "interfaces/software"; import softwareAPI from "services/entities/software"; -import { getPathWithQueryParams } from "utilities/url"; import { SELF_SERVICE_TOOLTIP } from "pages/SoftwarePage/helpers"; import Card from "components/Card"; -import ActionsDropdown from "components/ActionsDropdown"; import TooltipWrapper from "components/TooltipWrapper"; -import DataSet from "components/DataSet"; import Icon from "components/Icon"; import Tag from "components/Tag"; +import Button from "components/buttons/Button"; import endpoints from "utilities/endpoints"; import URL_PREFIX from "router/url_prefix"; import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; import CustomLink from "components/CustomLink"; -import SoftwareDetailsWidget from "pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareDetailsWidget"; +import InstallerDetailsWidget from "pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget"; import CategoriesEndUserExperienceModal from "pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal"; import DeleteSoftwareModal from "../DeleteSoftwareModal"; import EditSoftwareModal from "../EditSoftwareModal"; import { - APP_STORE_APP_DROPDOWN_OPTIONS, - SOFTWARE_PACKAGE_DROPDOWN_OPTIONS, + APP_STORE_APP_ACTION_OPTIONS, + SOFTWARE_PACKAGE_ACTION_OPTIONS, downloadFile, } from "./helpers"; -import AutomaticInstallModal from "../AutomaticInstallModal"; +import InstallerStatusTable from "./InstallerStatusTable"; +import InstallerPoliciesTable from "./InstallerPoliciesTable"; const baseClass = "software-installer-card"; @@ -92,52 +90,6 @@ const STATUS_DISPLAY_OPTIONS: Record< }, }; -interface IInstallerStatusCountProps { - softwareId: number; - status: SoftwareInstallDisplayStatus; - count: number; - teamId?: number; -} - -const InstallerStatusCount = ({ - softwareId, - status, - count, - teamId, -}: IInstallerStatusCountProps) => { - const displayData = STATUS_DISPLAY_OPTIONS[status]; - const linkUrl = getPathWithQueryParams(PATHS.MANAGE_HOSTS, { - software_title_id: softwareId, - software_status: status, - team_id: teamId, - }); - - return ( - -
    - -
    {displayData.displayName}
    -
    - - } - value={ - - {count} hosts - - } - /> - ); -}; - interface IActionsDropdownProps { installerType: "package" | "vpp"; onDownloadClick: () => void; @@ -145,7 +97,7 @@ interface IActionsDropdownProps { onEditSoftwareClick: () => void; } -const SoftwareActionsDropdown = ({ +const SoftwareActionButtons = ({ installerType, onDownloadClick, onDeleteClick, @@ -155,26 +107,10 @@ const SoftwareActionsDropdown = ({ const { gitops_mode_enabled: gitOpsModeEnabled, repository_url: repoURL } = config?.gitops || {}; - const onSelect = (action: string) => { - switch (action) { - case "download": - onDownloadClick(); - break; - case "delete": - onDeleteClick(); - break; - case "edit": - onEditSoftwareClick(); - break; - default: - // noop - } - }; - let options = installerType === "package" - ? [...SOFTWARE_PACKAGE_DROPDOWN_OPTIONS] - : [...APP_STORE_APP_DROPDOWN_OPTIONS]; + ? [...SOFTWARE_PACKAGE_ACTION_OPTIONS] + : [...APP_STORE_APP_ACTION_OPTIONS]; if (gitOpsModeEnabled) { const tooltipContent = ( @@ -206,15 +142,39 @@ const SoftwareActionsDropdown = ({ }); } + // Map action values to handlers + const actionHandlers = { + download: onDownloadClick, + delete: onDeleteClick, + edit: onEditSoftwareClick, + }; + return (
    - + {options.map((option) => { + const ButtonContent = ( + + ); + + // If there's a tooltip, wrap the button + return option.tooltipContent ? ( + + {ButtonContent} + + ) : ( + ButtonContent + ); + })}
    ); }; @@ -234,6 +194,7 @@ interface ISoftwareInstallerCardProps { softwareInstaller: ISoftwarePackage | IAppStoreApp; onDelete: () => void; refetchSoftwareTitle: () => void; + isLoading: boolean; } // NOTE: This component is dependent on having either a software package @@ -250,10 +211,19 @@ const SoftwareInstallerCard = ({ teamId, onDelete, refetchSoftwareTitle, + isLoading, }: ISoftwareInstallerCardProps) => { const installerType = isSoftwarePackage(softwareInstaller) ? "package" : "vpp"; + const isFleetMaintainedApp = + "fleet_maintained_app_id" in softwareInstaller && + !!softwareInstaller.fleet_maintained_app_id; + + const { + automatic_install_policies: automaticInstallPolicies, + } = softwareInstaller; + const { isGlobalAdmin, isGlobalMaintainer, @@ -265,9 +235,6 @@ const SoftwareInstallerCard = ({ const [showEditSoftwareModal, setShowEditSoftwareModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showAutomaticInstallModal, setShowAutomaticInstallModal] = useState( - false - ); const onEditSoftwareClick = () => { setShowEditSoftwareModal(true); @@ -340,26 +307,27 @@ const SoftwareInstallerCard = ({
    -
    - {Array.isArray(softwareInstaller.automatic_install_policies) && - softwareInstaller.automatic_install_policies.length > 0 && ( + {Array.isArray(automaticInstallPolicies) && + automaticInstallPolicies.length > 0 && ( - setShowAutomaticInstallModal(true)} - /> + )} {isSelfService && ( @@ -376,7 +344,7 @@ const SoftwareInstallerCard = ({
    {showActions && ( -
    -
    - + - -
    + {automaticInstallPolicies && ( +
    + +
    + )} {showEditSoftwareModal && ( )} - {showAutomaticInstallModal && - softwareInstaller?.automatic_install_policies && - softwareInstaller?.automatic_install_policies.length > 0 && ( - setShowAutomaticInstallModal(false)} - /> - )} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss index 498672f582..72d17049a5 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss @@ -5,6 +5,15 @@ align-items: center; gap: $pad-medium; + &__actions { + display: flex; + } + + &__installer-status-table, + &__installer-policies-table { + width: 100%; + } + &__row-1 { display: flex; width: 100%; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts index 3c29e48040..11a878dc08 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts @@ -1,24 +1,37 @@ -const DOWNLOAD_OPTION = { - label: "Download", +import { IconNames } from "components/icons"; +import { ReactNode } from "react"; + +type ISoftwareOption = { + value: string; + disabled: boolean; + iconName: IconNames; + tooltipContent?: ReactNode; +}; + +const DOWNLOAD_OPTION: ISoftwareOption = { value: "download", + disabled: false, + iconName: "download", }; -const EDIT_OPTION = { - label: "Edit", +const EDIT_OPTION: ISoftwareOption = { value: "edit", + disabled: false, + iconName: "pencil", }; -const DELETE_OPTION = { - label: "Delete", +const DELETE_OPTION: ISoftwareOption = { value: "delete", + disabled: false, + iconName: "trash", }; -export const SOFTWARE_PACKAGE_DROPDOWN_OPTIONS = [ +export const SOFTWARE_PACKAGE_ACTION_OPTIONS = [ DOWNLOAD_OPTION, EDIT_OPTION, DELETE_OPTION, ] as const; -export const APP_STORE_APP_DROPDOWN_OPTIONS = [ +export const APP_STORE_APP_ACTION_OPTIONS = [ EDIT_OPTION, DELETE_OPTION, ] as const; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx new file mode 100644 index 0000000000..cc76dee86d --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx @@ -0,0 +1,68 @@ +/** software/titles/:id > First section */ + +import React from "react"; + +import { InjectedRouter } from "react-router"; + +import { + formatSoftwareType, + isIpadOrIphoneSoftwareSource, + ISoftwareTitleDetails, +} from "interfaces/software"; + +import Card from "components/Card"; +import SoftwareDetailsSummary from "pages/SoftwarePage/components/cards/SoftwareDetailsSummary"; +import TitleVersionsTable from "./TitleVersionsTable"; + +interface ISoftwareSummaryCard { + title: ISoftwareTitleDetails; + softwareId: number; + teamId?: number; + isAvailableForInstall?: boolean; + isLoading?: boolean; + router: InjectedRouter; +} + +const baseClass = "software-summary-card"; + +const SoftwareSummaryCard = ({ + teamId, + softwareId, + isAvailableForInstall, + title, + isLoading = false, + router, +}: ISoftwareSummaryCard) => { + // Hide versions card for tgz_packages only + if (title.source === "tgz_packages") return null; + + return ( + + + + + ); +}; + +export default SoftwareSummaryCard; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tests.tsx new file mode 100644 index 0000000000..16751aa81a --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tests.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { screen, render } from "@testing-library/react"; +import { ISoftwareTitleVersion } from "interfaces/software"; +import TitleVersionsTable from "./TitleVersionsTable"; + +// TODO: figure out how to mock the router properly. +const mockRouter = { + push: jest.fn(), + replace: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + go: jest.fn(), + setRouteLeaveHook: jest.fn(), + isActive: jest.fn(), + createHref: jest.fn(), + createPath: jest.fn(), +}; + +describe("TitleVersionsTable", () => { + it("renders version names as links and footer info", () => { + const versions = [ + { id: 10, version: "1.2.3", vulnerabilities: [] }, + { id: 11, version: "1.2.4", vulnerabilities: [] }, + ]; + + render( + + ); + + // There should be one cell with a link for the version + const cells = screen.getAllByRole("cell"); + expect(cells).toHaveLength(8); + expect(screen.getByText(/1.2.3/i)).toBeInTheDocument(); + expect(screen.getByText(/1.2.4/i)).toBeInTheDocument(); + + // Version count should be shown + expect(screen.getByText(/2 versions/i)).toBeInTheDocument(); + + // Last updated info should be shown + expect(screen.getByText(/updated/i)).toBeInTheDocument(); + }); + + it("renders empty state if no versions detected", () => { + const versions: ISoftwareTitleVersion[] = []; + + render( + + ); + + const cells = screen.queryAllByRole("cell"); + expect(cells).toHaveLength(0); + + // Version count should not be shown + expect(screen.queryByText(/0 versions/i)).not.toBeInTheDocument(); + + // Last updated info should be shown + expect(screen.getByText(/updated/i)).toBeInTheDocument(); + + // Empty state should be shown + expect(screen.getByText(/no versions detected/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tsx similarity index 64% rename from frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx rename to frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tsx index 19833cbbd7..645b1e7f14 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable.tsx @@ -14,15 +14,17 @@ import TableCount from "components/TableContainer/TableCount"; import EmptyTable from "components/EmptyTable"; import CustomLink from "components/CustomLink"; import LastUpdatedText from "components/LastUpdatedText"; +import Card from "components/Card"; -import generateSoftwareTitleDetailsTableConfig from "./SoftwareTitleDetailsTableConfig"; +import generateSoftwareTitleVersionsTableConfig from "./TitleVersionsTableConfig"; const DEFAULT_SORT_HEADER = "hosts_count"; const DEFAULT_SORT_DIRECTION = "desc"; +const DEFAULT_PAGE_SIZE = 5; -const baseClass = "software-title-details-table"; +const baseClass = "software-title-versions-table"; -const SoftwareLastUpdatedInfo = (lastUpdatedAt: string) => { +const TitleVersionsLastUpdatedInfo = (lastUpdatedAt: string) => { return ( { const NoVersionsDetected = (isAvailableForInstall = false): JSX.Element => { return ( - - Expecting to see versions?{" "} - - - ) - } - /> + + + Expecting to see versions?{" "} + + + ) + } + /> + ); }; -interface ISoftwareTitleDetailsTableProps { +interface ITitleVersionsTableProps { router: InjectedRouter; data: ISoftwareTitleVersion[]; isLoading: boolean; @@ -79,7 +83,7 @@ interface IRowProps extends Row { }; } -const SoftwareTitleDetailsTable = ({ +const TitleVersionsTable = ({ router, data, isLoading, @@ -87,7 +91,7 @@ const SoftwareTitleDetailsTable = ({ isIPadOSOrIOSApp, isAvailableForInstall, countsUpdatedAt, -}: ISoftwareTitleDetailsTableProps) => { +}: ITitleVersionsTableProps) => { const handleRowSelect = (row: IRowProps) => { if (row.original.id) { const softwareVersionId = row.original.id; @@ -103,18 +107,17 @@ const SoftwareTitleDetailsTable = ({ const softwareTableHeaders = useMemo( () => - generateSoftwareTitleDetailsTableConfig({ - router, + generateSoftwareTitleVersionsTableConfig({ teamId: teamIdForApi, isIPadOSOrIOSApp, }), - [router, teamIdForApi, isIPadOSOrIOSApp] + [teamIdForApi, isIPadOSOrIOSApp] ); const renderVersionsCount = () => ( <> - - {countsUpdatedAt && SoftwareLastUpdatedInfo(countsUpdatedAt)} + {data?.length > 0 && } + {countsUpdatedAt && TitleVersionsLastUpdatedInfo(countsUpdatedAt)} ); @@ -129,12 +132,14 @@ const SoftwareTitleDetailsTable = ({ isAllPagesSelected={false} defaultSortHeader={DEFAULT_SORT_HEADER} defaultSortDirection={DEFAULT_SORT_DIRECTION} - disablePagination + pageSize={DEFAULT_PAGE_SIZE} + isClientSidePagination disableMultiRowSelect onSelectSingleRow={handleRowSelect} renderCount={renderVersionsCount} + hideFooter={data?.length <= DEFAULT_PAGE_SIZE} // Removes footer space /> ); }; -export default SoftwareTitleDetailsTable; +export default TitleVersionsTable; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTableConfig.tsx similarity index 89% rename from frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx rename to frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTableConfig.tsx index d4ae2feb72..eb0740a507 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTableConfig.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { InjectedRouter } from "react-router"; import { ISoftwareTitleVersion, @@ -12,10 +11,9 @@ import TextCell from "components/TableContainer/DataTable/TextCell"; import ViewAllHostsLink from "components/ViewAllHostsLink"; import LinkCell from "components/TableContainer/DataTable/LinkCell"; -import VulnerabilitiesCell from "../../components/tables/VulnerabilitiesCell"; +import VulnerabilitiesCell from "../../../components/tables/VulnerabilitiesCell"; -interface ISoftwareTitleDetailsTableConfigProps { - router: InjectedRouter; +interface ISoftwareTitleVersionsTableConfigProps { teamId?: number; isIPadOSOrIOSApp: boolean; } @@ -46,11 +44,10 @@ interface IVulnCellProps extends ICellProps { }; } -const generateSoftwareTitleDetailsTableConfig = ({ - router, +const generateSoftwareTitleVersionsTableConfig = ({ teamId, isIPadOSOrIOSApp, -}: ISoftwareTitleDetailsTableConfigProps) => { +}: ISoftwareTitleVersionsTableConfigProps) => { const tableHeaders = [ { title: "Version", @@ -132,4 +129,4 @@ const generateSoftwareTitleDetailsTableConfig = ({ return tableHeaders; }; -export default generateSoftwareTitleDetailsTableConfig; +export default generateSoftwareTitleVersionsTableConfig; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/_styles.scss new file mode 100644 index 0000000000..3792e70a1a --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/_styles.scss @@ -0,0 +1,5 @@ +.software-title-versions-table { + .empty-table__container { + margin: $pad-small auto; // Creates 32px total + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/index.ts new file mode 100644 index 0000000000..f0aaf461df --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/TitleVersionsTable/index.ts @@ -0,0 +1 @@ +export { default } from "./TitleVersionsTable"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/_styles.scss new file mode 100644 index 0000000000..d3a47eeb16 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/_styles.scss @@ -0,0 +1,2 @@ +.software-summary-card { +} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/index.ts new file mode 100644 index 0000000000..c9913d259a --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareSummaryCard"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx index f66d7c186b..ef162894c2 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx @@ -10,11 +10,7 @@ import paths from "router/paths"; import useTeamIdParam from "hooks/useTeamIdParam"; import { AppContext } from "context/app"; import { ignoreAxiosError } from "interfaces/errors"; -import { - ISoftwareTitleDetails, - formatSoftwareType, - isIpadOrIphoneSoftwareSource, -} from "interfaces/software"; +import { ISoftwareTitleDetails } from "interfaces/software"; import { APP_CONTEXT_ALL_TEAMS_ID, APP_CONTEXT_NO_TEAM_ID, @@ -30,11 +26,8 @@ import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; import Spinner from "components/Spinner"; import MainContent from "components/MainContent"; import TeamsHeader from "components/TeamsHeader"; -import Card from "components/Card"; - -import SoftwareDetailsSummary from "../components/cards/SoftwareDetailsSummary"; -import SoftwareTitleDetailsTable from "./SoftwareTitleDetailsTable"; import DetailsNoHosts from "../components/cards/DetailsNoHosts"; +import SoftwareSummaryCard from "./SoftwareSummaryCard"; import SoftwareInstallerCard from "./SoftwareInstallerCard"; import { getInstallerCardInfo } from "./helpers"; @@ -163,31 +156,21 @@ const SoftwareTitleDetailsPage = ({ teamId={currentTeamId ?? APP_CONTEXT_NO_TEAM_ID} onDelete={onDeleteInstaller} refetchSoftwareTitle={refetchSoftwareTitle} + isLoading={isSoftwareTitleLoading} /> ); }; - const renderSoftwareVersionsCard = (title: ISoftwareTitleDetails) => { - // Hide versions card for tgz_packages only - if (title.source === "tgz_packages") return null; - + const renderSoftwareSummaryCard = (title: ISoftwareTitleDetails) => { return ( - -

    Versions

    - -
    + ); }; @@ -208,26 +191,8 @@ const SoftwareTitleDetailsPage = ({ if (softwareTitle) { return ( <> - + {renderSoftwareSummaryCard(softwareTitle)} {renderSoftwareInstallerCard(softwareTitle)} - {renderSoftwareVersionsCard(softwareTitle)} ); } diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts deleted file mode 100644 index 2e2c71c6e0..0000000000 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SoftwareTitleDetailsTable"; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss index 4c68ad56d7..fa9b2aad9d 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss @@ -12,3 +12,9 @@ font-size: $small; } } + +.software-summary-and-versions { + display: flex; + flex-direction: column; + gap: $pad-medium; +} diff --git a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx index 2198b324f7..39eaae879c 100644 --- a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx +++ b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx @@ -23,7 +23,7 @@ interface ISoftwareDetailsSummaryProps { type?: string; hosts: number; countsUpdatedAt?: string; - /** The query param that will be added when user clicks on "View all hosts" link */ + /** The query param that will be added when user clicks on the host count */ queryParams: QueryParams; name?: string; source?: string; diff --git a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/_styles.scss b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/_styles.scss index 5006d85624..3e2fdce2f0 100644 --- a/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/_styles.scss +++ b/frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/_styles.scss @@ -1,9 +1,4 @@ .software-details-summary { - background-color: $core-white; - padding: $pad-xxlarge; - border: 1px solid $ui-fleet-black-10; - border-radius: $border-radius-xxlarge; - box-shadow: $box-shadow; display: flex; gap: $pad-medium;