mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Frontend Tech Debt: Software card API dependency moved to page level component (#7672)
This commit is contained in:
parent
b23374ad16
commit
24a7b1f8fd
3 changed files with 135 additions and 130 deletions
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue