Add ability to refresh host vitals without page reload (#2559)

* Implement useQuery refetch for host details
* Disable useQuery auto refetch options
* Change refetch message
* Refactor software table
* Refactor users and software states to useQuery
This commit is contained in:
gillespi314 2021-10-19 11:52:57 -05:00 committed by GitHub
parent ce8a8e371a
commit 974485e639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 101 additions and 60 deletions

View file

@ -0,0 +1 @@
* Add functionality to refetch and update host vitals without page reload

View file

@ -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<number | null>(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<IHostResponse, Error, IHost>(
"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 && (
<TableContainer
columns={tableHeaders}
data={generateSoftwareDataSet(softwareState)}
data={softwareState}
isLoading={isLoadingHost}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
@ -663,7 +722,7 @@ const HostDetailsPage = ({
onClick={onRefetchHost}
>
{showRefetchLoadingSpinner
? "Fetching, try refreshing this page in just a moment."
? "Fetching fresh vitals...this may take a moment"
: "Refetch"}
</Button>
</div>

View file

@ -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<string, string> = {
apt_sources: "Package (APT)",
deb_packages: "Package (deb)",
@ -61,6 +57,11 @@ const TYPE_CONVERSION: Record<string, string> = {
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) => <TextCell value={cellProps.cell.value} />,
accessor: "source",
Cell: (cellProps) => (
<TextCell value={cellProps.cell.value} formatter={formatSoftwareType} />
),
},
{
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;