diff --git a/frontend/pages/Homepage/Homepage.tsx b/frontend/pages/Homepage/Homepage.tsx index b623fb468f..a7dd152846 100644 --- a/frontend/pages/Homepage/Homepage.tsx +++ b/frontend/pages/Homepage/Homepage.tsx @@ -2,6 +2,7 @@ import React, { useContext, useState } from "react"; import { useQuery } from "react-query"; import { AppContext } from "context/app"; import { find } from "lodash"; +import paths from "router/paths"; import { IEnrollSecret, @@ -21,9 +22,11 @@ import { ITeam } from "interfaces/team"; import enrollSecretsAPI from "services/entities/enroll_secret"; import hostSummaryAPI from "services/entities/host_summary"; import macadminsAPI from "services/entities/macadmins"; +import softwareAPI, { ISoftwareResponse } from "services/entities/software"; import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams"; import sortUtils from "utilities/sort"; import { PLATFORM_DROPDOWN_OPTIONS } from "utilities/constants"; +import { ITableQueryData } from "components/TableContainer"; import TeamsDropdown from "components/TeamsDropdown"; import Spinner from "components/Spinner"; @@ -50,6 +53,7 @@ const Homepage = (): JSX.Element => { const { config, currentTeam, + availableTeams, isGlobalAdmin, isGlobalMaintainer, isTeamAdmin, @@ -69,7 +73,12 @@ const Homepage = (): JSX.Element => { const [onlineCount, setOnlineCount] = useState(0); const [offlineCount, setOfflineCount] = useState(0); const [showActivityFeedTitle, setShowActivityFeedTitle] = useState(false); - const [showSoftwareUI, setShowSoftwareUI] = useState(false); + const [softwareTitleDetail, setSoftwareTitleDetail] = useState< + JSX.Element | string | null + >(""); + const [softwareNavTabIndex, setSoftwareNavTabIndex] = useState(0); + const [softwarePageIndex, setSoftwarePageIndex] = useState(0); + const [softwareActionUrl, setSoftwareActionUrl] = useState(); const [showMunkiCard, setShowMunkiCard] = useState(true); const [showAddHostsModal, setShowAddHostsModal] = useState(false); const [showOperatingSystemsUI, setShowOperatingSystemsUI] = useState(false); @@ -172,6 +181,56 @@ const Homepage = (): JSX.Element => { } ); + const isSoftwareEnabled = config?.features?.enable_software_inventory; + const SOFTWARE_DEFAULT_SORT_DIRECTION = "desc"; + const SOFTWARE_DEFAULT_SORT_HEADER = "hosts_count"; + const SOFTWARE_DEFAULT_PAGE_SIZE = 8; + + const { + data: software, + isFetching: isSoftwareFetching, + error: errorSoftware, + } = useQuery( + [ + "software", + { + pageIndex: softwarePageIndex, + pageSize: SOFTWARE_DEFAULT_PAGE_SIZE, + sortDirection: SOFTWARE_DEFAULT_SORT_DIRECTION, + sortHeader: SOFTWARE_DEFAULT_SORT_HEADER, + teamId: currentTeam?.id, + vulnerable: !!softwareNavTabIndex, // we can take the tab index as a boolean to represent the vulnerable flag :) + }, + ], + () => + softwareAPI.load({ + page: softwarePageIndex, + perPage: SOFTWARE_DEFAULT_PAGE_SIZE, + orderKey: SOFTWARE_DEFAULT_SORT_HEADER, + orderDir: SOFTWARE_DEFAULT_SORT_DIRECTION, + vulnerable: !!softwareNavTabIndex, // we can take the tab index as a boolean to represent the vulnerable flag :) + teamId: currentTeam?.id, + }), + { + enabled: + (isSoftwareEnabled && isOnGlobalTeam) || + !!availableTeams?.find((t) => t.id === currentTeam?.id), + keepPreviousData: true, + staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval + onSuccess: (data) => { + if (data.software?.length !== 0) { + setSoftwareTitleDetail && + setSoftwareTitleDetail( + + ); + } + }, + } + ); + const { isFetching: isMacAdminsFetching, error: errorMacAdmins } = useQuery< IMacadminAggregate, Error @@ -268,6 +327,33 @@ const Homepage = (): JSX.Element => { ), }); + // NOTE: this is called once on the initial rendering. The initial render of + // the TableContainer child component will call this handler. + const onSoftwareQueryChange = async ({ + pageIndex: newPageIndex, + }: ITableQueryData) => { + if (softwarePageIndex !== newPageIndex) { + setSoftwarePageIndex(newPageIndex); + } + }; + + const onSoftwareTabChange = (index: number) => { + const { MANAGE_SOFTWARE } = paths; + setSoftwareNavTabIndex(index); + setSoftwareActionUrl && + setSoftwareActionUrl( + index === 1 ? `${MANAGE_SOFTWARE}?vulnerable=true` : MANAGE_SOFTWARE + ); + }; + + // TODO: Rework after backend is adjusted to differentiate empty search/filter results from + // collecting inventory + const isCollectingInventory = + !currentTeam?.id && + !softwarePageIndex && + !software?.software && + software?.counts_updated_at === null; + const HostsStatusCard = useInfoCard({ title: "", children: ( @@ -314,12 +400,20 @@ const Homepage = (): JSX.Element => { text: "View all software", to: "software", }, - showTitle: showSoftwareUI, + actionUrl: softwareActionUrl, + titleDetail: softwareTitleDetail, + showTitle: !isSoftwareFetching, children: ( ), }); diff --git a/frontend/pages/Homepage/cards/Software/Software.tsx b/frontend/pages/Homepage/cards/Software/Software.tsx index 4fad8c0df3..f19cc9409a 100644 --- a/frontend/pages/Homepage/cards/Software/Software.tsx +++ b/frontend/pages/Homepage/cards/Software/Software.tsx @@ -1,142 +1,49 @@ -import React, { useContext, useState } from "react"; -import { useQuery } from "react-query"; +import React from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; -import { AppContext } from "context/app"; -import paths from "router/paths"; -import configAPI from "services/entities/config"; -import softwareAPI, { ISoftwareResponse } from "services/entities/software"; - import TabsWrapper from "components/TabsWrapper"; -import TableContainer, { ITableQueryData } from "components/TableContainer"; +import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; -import LastUpdatedText from "components/LastUpdatedText/LastUpdatedText"; import generateTableHeaders from "./SoftwareTableConfig"; import EmptySoftware from "../../../software/components/EmptySoftware"; interface ISoftwareCardProps { - currentTeamId?: number; - showSoftwareUI: boolean; - setShowSoftwareUI: (showSoftwareTitle: boolean) => void; - setActionURL?: (url: string) => void; - setTitleDetail?: (content: JSX.Element | string | null) => void; + errorSoftware: Error | null; + isCollectingInventory: boolean; + isSoftwareFetching: boolean; + isSoftwareEnabled?: boolean; + software: any; + pageIndex: number; + navTabIndex: any; + onTabChange: any; + onQueryChange: any; } -const DEFAULT_SORT_DIRECTION = "desc"; -const DEFAULT_SORT_HEADER = "hosts_count"; -const DEFAULT_PAGE_SIZE = 8; +const SOFTWARE_DEFAULT_SORT_DIRECTION = "desc"; +const SOFTWARE_DEFAULT_SORT_HEADER = "hosts_count"; +const SOFTWARE_DEFAULT_PAGE_SIZE = 8; + const baseClass = "home-software"; const Software = ({ - currentTeamId, - showSoftwareUI, - setShowSoftwareUI, - setActionURL, - setTitleDetail, + errorSoftware, + isCollectingInventory, + isSoftwareFetching, + isSoftwareEnabled, + navTabIndex, + onTabChange, + onQueryChange, + software, }: ISoftwareCardProps): JSX.Element => { - const [navTabIndex, setNavTabIndex] = useState(0); - const [pageIndex, setPageIndex] = useState(0); - const [isSoftwareEnabled, setIsSoftwareEnabled] = useState(false); - - const { availableTeams, currentTeam, isOnGlobalTeam } = useContext( - AppContext - ); - - const { data: config } = useQuery(["config"], configAPI.loadAll, { - onSuccess: (data) => { - setIsSoftwareEnabled(data?.features?.enable_software_inventory); - }, - }); - - const { - data: software, - isFetching: isSoftwareFetching, - error: errorSoftware, - } = useQuery( - [ - "software", - { - pageIndex, - pageSize: DEFAULT_PAGE_SIZE, - sortDirection: DEFAULT_SORT_DIRECTION, - sortHeader: DEFAULT_SORT_HEADER, - teamId: currentTeamId, - vulnerable: !!navTabIndex, // we can take the tab index as a boolean to represent the vulnerable flag :) - }, - ], - () => - softwareAPI.load({ - page: pageIndex, - perPage: DEFAULT_PAGE_SIZE, - orderKey: DEFAULT_SORT_HEADER, - orderDir: DEFAULT_SORT_DIRECTION, - vulnerable: !!navTabIndex, // we can take the tab index as a boolean to represent the vulnerable flag :) - teamId: currentTeamId, - }), - { - enabled: - isOnGlobalTeam || - !!availableTeams?.find((t) => t.id === currentTeam?.id), - keepPreviousData: true, - staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval - onSuccess: (data) => { - setShowSoftwareUI(true); - if (isSoftwareEnabled && data.software?.length !== 0) { - setTitleDetail && - setTitleDetail( - - ); - } - }, - onError: () => { - setShowSoftwareUI(true); - }, - } - ); - - // TODO: Rework after backend is adjusted to differentiate empty search/filter results from - // collecting inventory - const isCollectingInventory = - !currentTeamId && - !pageIndex && - !software?.software && - software?.counts_updated_at === null; - - if (isCollectingInventory) { - setTitleDetail && setTitleDetail(""); - } - - // NOTE: this is called once on the initial rendering. The initial render of - // the TableContainer child component will call this handler. - const onQueryChange = async ({ - pageIndex: newPageIndex, - }: ITableQueryData) => { - if (pageIndex !== newPageIndex) { - setPageIndex(newPageIndex); - } - }; - - const onTabChange = (index: number) => { - const { MANAGE_SOFTWARE } = paths; - setNavTabIndex(index); - setActionURL && - setActionURL( - index === 1 ? `${MANAGE_SOFTWARE}?vulnerable=true` : MANAGE_SOFTWARE - ); - }; - const tableHeaders = generateTableHeaders(); // Renders opaque information as host information is loading - const opacity = showSoftwareUI ? { opacity: 1 } : { opacity: 0 }; + const opacity = isSoftwareFetching ? { opacity: 0 } : { opacity: 1 }; return (
- {!showSoftwareUI && ( + {isSoftwareFetching && (
@@ -156,8 +63,8 @@ const Software = ({ columns={tableHeaders} data={software?.software || []} isLoading={isSoftwareFetching} - defaultSortHeader={DEFAULT_SORT_HEADER} - defaultSortDirection={DEFAULT_SORT_DIRECTION} + defaultSortHeader={"hosts_count"} + defaultSortDirection={SOFTWARE_DEFAULT_SORT_DIRECTION} hideActionButton resultsTitle={"software"} emptyComponent={() => @@ -171,7 +78,7 @@ const Software = ({ isAllPagesSelected={false} disableCount disableActionButton - pageSize={DEFAULT_PAGE_SIZE} + pageSize={SOFTWARE_DEFAULT_PAGE_SIZE} onQueryChange={onQueryChange} /> )} @@ -184,8 +91,8 @@ const Software = ({ columns={tableHeaders} data={software?.software || []} isLoading={isSoftwareFetching} - defaultSortHeader={DEFAULT_SORT_HEADER} - defaultSortDirection={DEFAULT_SORT_DIRECTION} + defaultSortHeader={SOFTWARE_DEFAULT_SORT_HEADER} + defaultSortDirection={SOFTWARE_DEFAULT_SORT_DIRECTION} hideActionButton resultsTitle={"software"} emptyComponent={() => @@ -199,7 +106,7 @@ const Software = ({ isAllPagesSelected={false} disableCount disableActionButton - pageSize={DEFAULT_PAGE_SIZE} + pageSize={SOFTWARE_DEFAULT_PAGE_SIZE} onQueryChange={onQueryChange} /> )} diff --git a/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx b/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx index ac5c2380d0..1432ee03ec 100644 --- a/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx +++ b/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx @@ -8,6 +8,7 @@ interface IInfoCardProps { title: string; titleDetail?: JSX.Element | string | null; description?: JSX.Element | string; + actionUrl?: string; children: React.ReactChild | React.ReactChild[]; action?: | { @@ -30,12 +31,15 @@ const useInfoCard = ({ title, titleDetail: defaultTitleDetail, description: defaultDescription, + actionUrl: defaultActionUrl, children, action, total_host_count, showTitle = true, }: IInfoCardProps): JSX.Element => { - const [actionLink, setActionURL] = useState(null); + const [actionLink, setActionURL] = useState( + defaultActionUrl || null + ); const [titleDetail, setTitleDetail] = useState( defaultTitleDetail || null );