Frontend Tech Debt: Software card API dependency moved to page level component (#7672)

This commit is contained in:
RachelElysia 2022-09-23 10:45:00 -04:00 committed by GitHub
parent b23374ad16
commit 24a7b1f8fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 135 additions and 130 deletions

View file

@ -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<string>();
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<ISoftwareResponse, Error>(
[
"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(
<LastUpdatedText
lastUpdatedAt={data.counts_updated_at}
whatToRetrieve={"software"}
/>
);
}
},
}
);
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: (
<Software
currentTeamId={currentTeam?.id}
setShowSoftwareUI={setShowSoftwareUI}
showSoftwareUI={showSoftwareUI}
errorSoftware={errorSoftware}
isCollectingInventory={isCollectingInventory}
isSoftwareFetching={isSoftwareFetching}
isSoftwareEnabled={isSoftwareEnabled}
software={software}
pageIndex={softwarePageIndex}
navTabIndex={softwareNavTabIndex}
onTabChange={onSoftwareTabChange}
onQueryChange={onSoftwareQueryChange}
/>
),
});

View file

@ -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<ISoftwareResponse, Error>(
[
"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(
<LastUpdatedText
lastUpdatedAt={data.counts_updated_at}
whatToRetrieve={"software"}
/>
);
}
},
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 (
<div className={baseClass}>
{!showSoftwareUI && (
{isSoftwareFetching && (
<div className="spinner">
<Spinner />
</div>
@ -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}
/>
)}

View file

@ -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<string | null>(null);
const [actionLink, setActionURL] = useState<string | null>(
defaultActionUrl || null
);
const [titleDetail, setTitleDetail] = useState<JSX.Element | string | null>(
defaultTitleDetail || null
);