diff --git a/changes/2361-refetch-host b/changes/2361-refetch-host new file mode 100644 index 0000000000..c34ca2413a --- /dev/null +++ b/changes/2361-refetch-host @@ -0,0 +1 @@ +* Add functionality to refetch and update host vitals without page reload \ No newline at end of file diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx index 66b28a728c..83ee4c33de 100644 --- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx @@ -50,10 +50,7 @@ import { generatePolicyTableHeaders, generatePolicyDataSet, } from "./HostPoliciesTable/HostPoliciesTableConfig"; -import { - generateSoftwareTableHeaders, - generateSoftwareDataSet, -} from "./SoftwareTable/SoftwareTableConfig"; +import generateSoftwareTableHeaders from "./SoftwareTable/SoftwareTableConfig"; import generateUsersTableHeaders from "./UsersTable/UsersTableConfig"; import { generatePackTableHeaders, @@ -116,6 +113,7 @@ const HostDetailsPage = ({ setPolicyDetailsModal(!showPolicyDetailsModal); }, [showPolicyDetailsModal, setPolicyDetailsModal]); + const [refetchStartTime, setRefetchStartTime] = useState(null); const [ showRefetchLoadingSpinner, setShowRefetchLoadingSpinner, @@ -131,6 +129,9 @@ const HostDetailsPage = ({ IQuery[] >("fleet queries", () => queryAPI.loadAll(), { enabled: !!hostIdFromURL, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, select: (data: IFleetQueriesResponse) => data.queries, }); @@ -140,6 +141,9 @@ const HostDetailsPage = ({ ITeam[] >("teams", () => teamAPI.loadAll(), { enabled: !!hostIdFromURL && canTransferTeam, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, select: (data: ITeamsResponse) => data.teams, }); @@ -148,13 +152,70 @@ const HostDetailsPage = ({ data: host, refetch: fullyReloadHost, } = useQuery( - "host", + ["host", hostIdFromURL], () => hostAPI.load(hostIdFromURL), { enabled: !!hostIdFromURL, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, select: (data: IHostResponse) => data.host, + + // The onSuccess method below will run each time react-query successfully fetches data from + // the hosts API through this useQuery hook. + // This includes the initial page load as well as whenever we call react-query's refetch method, + // which above we renamed to fullyReloadHost. For example, we use fullyReloadHost with the refetch + // button and also after actions like team transfers. onSuccess: (returnedHost) => { + setSoftwareState(returnedHost.software); + setUsersState(returnedHost.users); setShowRefetchLoadingSpinner(returnedHost.refetch_requested); + + if (returnedHost.refetch_requested) { + // If the API reports that a Fleet refetch request is pending, we want to check back for fresh + // host details. Here we set a one second timeout and poll the API again using + // fullyReloadHost. We will repeat this process with each onSuccess cycle for a total of + // 60 seconds or until the API reports that the Fleet refetch request has been resolved + // or that the host has gone offline. + if (!refetchStartTime) { + // If our 60 second timer wasn't already started (e.g., if a refetch was pending when + // the first page loads), we start it now if the host is online. If the host is offline, + // we skip the refetch on page load. + if (returnedHost.status === "online") { + setRefetchStartTime(Date.now()); + setTimeout(() => { + fullyReloadHost(); + }, 1000); + } else { + setShowRefetchLoadingSpinner(false); + } + } else { + const totalElapsedTime = Date.now() - refetchStartTime; + if (totalElapsedTime < 60000) { + if (returnedHost.status === "online") { + setTimeout(() => { + fullyReloadHost(); + }, 1000); + } else { + dispatch( + renderFlash( + "error", + `This host is offline. Please try refetching host vitals later.` + ) + ); + setShowRefetchLoadingSpinner(false); + } + } else { + dispatch( + renderFlash( + "error", + `We're having trouble fetching fresh vitals for this host. Please try again later.` + ) + ); + setShowRefetchLoadingSpinner(false); + } + } + } }, onError: (error) => { console.log(error); @@ -166,34 +227,27 @@ const HostDetailsPage = ({ ); useEffect(() => { - if (host) { - setUsersState(host.users); - setSoftwareState(host.software); - } - }, [host]); - - useEffect(() => { - if (host) { - setUsersState(() => { - return host.users.filter((user) => { + setUsersState(() => { + return ( + host?.users.filter((user) => { return user.username .toLowerCase() .includes(usersSearchString.toLowerCase()); - }); - }); - } + }) || [] + ); + }); }, [usersSearchString]); useEffect(() => { - if (host) { - setSoftwareState(() => { - return host.software.filter((softwareItem) => { + setSoftwareState(() => { + return ( + host?.software.filter((softwareItem) => { return softwareItem.name .toLowerCase() .includes(softwareSearchString.toLowerCase()); - }); - }); - } + }) || [] + ); + }); }, [softwareSearchString]); // returns a mixture of props from host @@ -276,13 +330,18 @@ const HostDetailsPage = ({ const onRefetchHost = async () => { if (host) { // Once the user clicks to refetch, the refetch loading spinner should continue spinning - // unless there is an error or until the user refreshes the page. + // unless there is an error. The spinner state is also controlled in the fullyReloadHost + // method. setShowRefetchLoadingSpinner(true); try { - await hostAPI.refetch(host); + await hostAPI.refetch(host).then(() => { + setRefetchStartTime(Date.now()); + setTimeout(() => fullyReloadHost(), 1000); + }); } catch (error) { console.log(error); dispatch(renderFlash("error", `Host "${host.hostname}" refetch error`)); + setShowRefetchLoadingSpinner(false); } } }; @@ -619,7 +678,7 @@ const HostDetailsPage = ({ {host?.software && ( {showRefetchLoadingSpinner - ? "Fetching, try refreshing this page in just a moment." + ? "Fetching fresh vitals...this may take a moment" : "Refetch"} diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTable/SoftwareTableConfig.tsx b/frontend/pages/hosts/HostDetailsPage/SoftwareTable/SoftwareTableConfig.tsx index 42cbc54a84..8ce0b47f13 100644 --- a/frontend/pages/hosts/HostDetailsPage/SoftwareTable/SoftwareTableConfig.tsx +++ b/frontend/pages/hosts/HostDetailsPage/SoftwareTable/SoftwareTableConfig.tsx @@ -1,16 +1,16 @@ import React from "react"; -import { Link } from "react-router"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id +import { Link } from "react-router"; import ReactTooltip from "react-tooltip"; import { isEmpty } from "lodash"; // import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; // TODO: Enable after backend has been updated to provide last_opened_at -import PATHS from "router/paths"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id +import PATHS from "router/paths"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import { ISoftware } from "interfaces/software"; import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png"; import QuestionIcon from "../../../../../assets/images/icon-question-16x16@2x.png"; -import Chevron from "../../../../../assets/images/icon-chevron-blue-16x16@2x.png"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id +import Chevron from "../../../../../assets/images/icon-chevron-blue-16x16@2x.png"; interface IHeaderProps { column: { @@ -37,10 +37,6 @@ interface IDataColumn { sortType?: string; } -interface ISoftwareTableData extends ISoftware { - type: string; -} - const TYPE_CONVERSION: Record = { apt_sources: "Package (APT)", deb_packages: "Package (deb)", @@ -61,6 +57,11 @@ const TYPE_CONVERSION: Record = { pkg_packages: "Package (pkg)", }; +const formatSoftwareType = (source: string) => { + const DICT = TYPE_CONVERSION; + return DICT[source] || "Unknown"; +}; + // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generateSoftwareTableHeaders = (): IDataColumn[] => { @@ -156,8 +157,10 @@ const generateSoftwareTableHeaders = (): IDataColumn[] => { /> ), disableSortBy: false, - accessor: "type", - Cell: (cellProps) => , + accessor: "source", + Cell: (cellProps) => ( + + ), }, { title: "Installed version", @@ -194,7 +197,6 @@ const generateSoftwareTableHeaders = (): IDataColumn[] => { // }, // sortType: "dateStrings", // }, - // TODO: Enable after manage hosts page has been updated to filter hosts by software id { title: "", Header: "", @@ -217,25 +219,4 @@ const generateSoftwareTableHeaders = (): IDataColumn[] => { ]; }; -const enhanceSoftwareData = (software: ISoftware[]): ISoftwareTableData[] => { - return Object.values(software).map((softwareItem) => { - return { - ...softwareItem, - // linkToFilteredHosts: `${PATHS.MANAGE_HOSTS}?software_id=${softwareItem.id}`, - type: TYPE_CONVERSION[softwareItem.source] || "Unknown", - }; - }); -}; - -const generateSoftwareDataSet = ( - software: ISoftware[] -): ISoftwareTableData[] => { - // Cannot pass undefined to enhanceSoftwareData - if (!software) { - return software; - } - - return [...enhanceSoftwareData(software)]; -}; - -export { generateSoftwareTableHeaders, generateSoftwareDataSet }; +export default generateSoftwareTableHeaders;