UI – update 4 Software > details pages with desired empty state for 404 responses (#16944)

## Addresses #16948

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling 2024-02-20 10:28:16 -08:00 committed by GitHub
parent 9ed0c193c8
commit 436e900d62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 156 additions and 143 deletions

View file

@ -4,7 +4,7 @@ import React, { useCallback, useContext } from "react";
import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { RouteComponentProps } from "react-router";
import { AxiosError } from "axios";
import { AxiosError, isAxiosError } from "axios";
import useTeamIdParam from "hooks/useTeamIdParam";
@ -18,7 +18,6 @@ import { IOperatingSystemVersion } from "interfaces/operating_system";
import { SUPPORT_LINK } from "utilities/constants";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import MainContent from "components/MainContent";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
@ -103,7 +102,13 @@ const SoftwareOSDetailsPage = ({
{
enabled: !!osVersionIdFromURL,
select: (data) => data.os_version,
onError: (error) => handlePageError(error),
onError: (error) => {
// 403s returned for both non-existent and non-accessable entities
// which we intentionally handle with the same empty state for security
if (isAxiosError(error) && error.response?.status !== 403) {
handlePageError(error);
}
},
}
);
@ -142,11 +147,7 @@ const SoftwareOSDetailsPage = ({
return <Spinner />;
}
if (isOsVersionError) {
return <TableDataError className={`${baseClass}__table-error`} />;
}
if (!osVersionDetails) {
if (!osVersionDetails && !isOsVersionError) {
return null;
}
@ -160,32 +161,35 @@ const SoftwareOSDetailsPage = ({
onTeamChange={onTeamChange}
/>
)}
<SoftwareDetailsSummary
title={osVersionDetails.name}
hosts={osVersionDetails.hosts_count}
queryParams={{
os_name: osVersionDetails.name_only,
os_version: osVersionDetails.version,
team_id: teamIdForApi,
}}
name={osVersionDetails.platform}
/>
{osVersionDetails.hosts_count === 0 ? (
{/* at this point, error can only be 403 per above handling */}
{isOsVersionError ? (
<DetailsNoHosts
header="OS not detected"
details={`No hosts ${teamIdForApi ? "on this team " : ""}have ${
osVersionDetails.name
} installed.`}
details={`No hosts ${
teamIdForApi ? "on this team " : ""
}have this OS installed.`}
/>
) : (
<Card
borderRadiusSize="large"
includeShadow
className={`${baseClass}__vulnerabilities-section`}
>
<h2>Vulnerabilities</h2>
{renderTable()}
</Card>
<>
<SoftwareDetailsSummary
title={osVersionDetails.name}
hosts={osVersionDetails.hosts_count}
queryParams={{
os_name: osVersionDetails.name_only,
os_version: osVersionDetails.version,
team_id: teamIdForApi,
}}
name={osVersionDetails.platform}
/>
<Card
borderRadiusSize="large"
includeShadow
className={`${baseClass}__vulnerabilities-section`}
>
<h2>Vulnerabilities</h2>
{renderTable()}
</Card>
</>
)}
</>
);

View file

@ -4,7 +4,7 @@ import React, { useCallback, useContext } from "react";
import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { RouteComponentProps } from "react-router";
import { AxiosError } from "axios";
import { AxiosError, isAxiosError } from "axios";
import useTeamIdParam from "hooks/useTeamIdParam";
@ -17,7 +17,6 @@ import softwareAPI, {
} from "services/entities/software";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import MainContent from "components/MainContent";
import TeamsHeader from "components/TeamsHeader";
import Card from "components/Card";
@ -75,7 +74,13 @@ const SoftwareTitleDetailsPage = ({
({ queryKey }) => softwareAPI.getSoftwareTitle(queryKey[0]),
{
select: (data) => data.software_title,
onError: (error) => handlePageError(error),
onError: (error) => {
// 403s returned for both non-existent and non-accessable entities
// which we intentionally handle with the same empty state for security
if (isAxiosError(error) && error.response?.status !== 403) {
handlePageError(error);
}
},
}
);
@ -91,11 +96,7 @@ const SoftwareTitleDetailsPage = ({
return <Spinner />;
}
if (isSoftwareTitleError) {
return <TableDataError className={`${baseClass}__table-error`} />;
}
if (!softwareTitle) {
if (!softwareTitle && !isSoftwareTitleError) {
return null;
}
return (
@ -108,36 +109,42 @@ const SoftwareTitleDetailsPage = ({
onTeamChange={onTeamChange}
/>
)}
<SoftwareDetailsSummary
title={softwareTitle.name}
type={formatSoftwareType(softwareTitle)}
versions={softwareTitle.versions?.length ?? 0}
hosts={softwareTitle.hosts_count}
queryParams={{ software_title_id: softwareId, team_id: teamIdForApi }}
name={softwareTitle.name}
source={softwareTitle.source}
/>
{softwareTitle.hosts_count === 0 ? (
{/* at this point, error can only be 403 per above handling */}
{isSoftwareTitleError ? (
<DetailsNoHosts
header="Software not detected"
details={`No hosts ${teamIdForApi ? "on this team " : ""}have ${
softwareTitle.name
} installed.`}
details={`No hosts ${
teamIdForApi ? "on this team " : ""
}have this software installed.`}
/>
) : (
<Card
borderRadiusSize="large"
includeShadow
className={`${baseClass}__versions-section`}
>
<h2>Versions</h2>
<SoftwareTitleDetailsTable
router={router}
data={softwareTitle.versions ?? []}
isLoading={isSoftwareTitleLoading}
teamIdForApi={teamIdForApi}
<>
<SoftwareDetailsSummary
title={softwareTitle.name}
type={formatSoftwareType(softwareTitle)}
versions={softwareTitle.versions?.length ?? 0}
hosts={softwareTitle.hosts_count}
queryParams={{
software_title_id: softwareId,
team_id: teamIdForApi,
}}
name={softwareTitle.name}
source={softwareTitle.source}
/>
</Card>
<Card
borderRadiusSize="large"
includeShadow
className={`${baseClass}__versions-section`}
>
<h2>Versions</h2>
<SoftwareTitleDetailsTable
router={router}
data={softwareTitle.versions ?? []}
isLoading={isSoftwareTitleLoading}
teamIdForApi={teamIdForApi}
/>
</Card>
</>
)}
</>
);

View file

@ -4,7 +4,7 @@ import React, { useCallback, useContext } from "react";
import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { RouteComponentProps } from "react-router";
import { AxiosError } from "axios";
import { AxiosError, isAxiosError } from "axios";
import useTeamIdParam from "hooks/useTeamIdParam";
@ -21,7 +21,6 @@ import hostsCountAPI, {
import { ISoftwareVersion, formatSoftwareType } from "interfaces/software";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import MainContent from "components/MainContent";
import TeamsHeader from "components/TeamsHeader";
import Card from "components/Card";
@ -78,7 +77,13 @@ const SoftwareVersionDetailsPage = ({
({ queryKey }) => softwareAPI.getSoftwareVersion(queryKey[0]),
{
select: (data) => data.software,
onError: (error) => handlePageError(error),
onError: (error) => {
// 403s returned for both non-existent and non-accessable entities
// which we intentionally handle with the same empty state for security
if (isAxiosError(error) && error.response?.status !== 403) {
handlePageError(error);
}
},
}
);
@ -109,11 +114,7 @@ const SoftwareVersionDetailsPage = ({
return <Spinner />;
}
if (isSoftwareVersionError) {
return <TableDataError className={`${baseClass}__table-error`} />;
}
if (!softwareVersion) {
if (!softwareVersion && !isSoftwareVersionError) {
return null;
}
@ -127,18 +128,8 @@ const SoftwareVersionDetailsPage = ({
onTeamChange={onTeamChange}
/>
)}
<SoftwareDetailsSummary
title={`${softwareVersion.name}, ${softwareVersion.version}`}
type={formatSoftwareType(softwareVersion)}
hosts={hostsCount ?? 0}
queryParams={{
software_version_id: softwareVersion.id,
team_id: teamIdForApi,
}}
name={softwareVersion.name}
source={softwareVersion.source}
/>
{softwareVersion.hosts_count === 0 ? (
{/* at this point, error can only be 403 per above handling */}
{isSoftwareVersionError ? (
<DetailsNoHosts
header="Software not detected"
details={`No hosts ${
@ -146,20 +137,33 @@ const SoftwareVersionDetailsPage = ({
}have this software installed.`}
/>
) : (
<Card
borderRadiusSize="large"
includeShadow
className={`${baseClass}__vulnerabilities-section`}
>
<h2 className="section__header">Vulnerabilities</h2>
<SoftwareVulnerabilitiesTable
data={softwareVersion.vulnerabilities ?? []}
itemName="software item"
isLoading={isSoftwareVersionLoading}
router={router}
teamIdForApi={teamIdForApi}
<>
<SoftwareDetailsSummary
title={`${softwareVersion.name}, ${softwareVersion.version}`}
type={formatSoftwareType(softwareVersion)}
hosts={hostsCount ?? 0}
queryParams={{
software_version_id: softwareVersion.id,
team_id: teamIdForApi,
}}
name={softwareVersion.name}
source={softwareVersion.source}
/>
</Card>
<Card
borderRadiusSize="large"
includeShadow
className={`${baseClass}__vulnerabilities-section`}
>
<h2 className="section__header">Vulnerabilities</h2>
<SoftwareVulnerabilitiesTable
data={softwareVersion.vulnerabilities ?? []}
itemName="software item"
isLoading={isSoftwareVersionLoading}
router={router}
teamIdForApi={teamIdForApi}
/>
</Card>
</>
)}
</>
);

View file

@ -4,7 +4,7 @@ import React, { useCallback, useContext } from "react";
import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { RouteComponentProps } from "react-router";
import { AxiosError } from "axios";
import { AxiosError, isAxiosError } from "axios";
import useTeamIdParam from "hooks/useTeamIdParam";
@ -17,7 +17,6 @@ import softwareVulnAPI, {
} from "services/entities/vulnerabilities";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
import MainContent from "components/MainContent";
import TeamsHeader from "components/TeamsHeader";
@ -80,7 +79,13 @@ const SoftwareVulnerabilityDetailsPage = ({
},
{
select: (data) => data.vulnerability,
onError: (error) => handlePageError(error),
onError: (error) => {
// 403s returned for both non-existent and non-accessable entities
// which we intentionally handle with the same empty state for security
if (isAxiosError(error) && error.response?.status !== 403) {
handlePageError(error);
}
},
}
);
@ -91,49 +96,37 @@ const SoftwareVulnerabilityDetailsPage = ({
[handleTeamChange]
);
const renderVulnTables = () => {
// always the case, just for typing
if (vuln) {
if (vuln.hosts_count === 0) {
return (
<DetailsNoHosts
header="Vulnerability not detected"
details={`No hosts ${
teamIdForApi ? "on this team " : ""
}are affected by ${vuln.cve}.`}
/>
);
}
return (
<>
{!!vuln.os_versions && vuln.os_versions.length > 0 && (
<SoftwareVulnOSVersions
osVersions={vuln.os_versions}
isPremiumTier={isPremiumTier ?? false}
router={router}
teamIdForApi={teamIdForApi}
/>
)}
{!!vuln.software && vuln.software.length > 0 && (
<SoftwareVulnSoftwareVersions
vulnSoftware={vuln.software}
isPremiumTier={isPremiumTier ?? false}
router={router}
teamIdForApi={teamIdForApi}
/>
)}
</>
);
}
};
const renderCards = (v: IVulnerability) => (
<>
<SoftwareVulnSummary
vuln={v}
isPremiumTier={isPremiumTier ?? false}
teamIdForApi={teamIdForApi}
/>
{!!v.os_versions && v.os_versions.length > 0 && (
<SoftwareVulnOSVersions
osVersions={v.os_versions}
isPremiumTier={isPremiumTier ?? false}
router={router}
teamIdForApi={teamIdForApi}
/>
)}
{!!v.software && v.software.length > 0 && (
<SoftwareVulnSoftwareVersions
vulnSoftware={v.software}
isPremiumTier={isPremiumTier ?? false}
router={router}
teamIdForApi={teamIdForApi}
/>
)}
</>
);
const renderContent = () => {
if (isVulnLoading || !vuln) {
return <Spinner />;
}
if (isVulnError) {
return <DataError />;
}
return (
<>
{isPremiumTier && (
@ -144,12 +137,17 @@ const SoftwareVulnerabilityDetailsPage = ({
onTeamChange={onTeamChange}
/>
)}
<SoftwareVulnSummary
vuln={vuln}
isPremiumTier={isPremiumTier ?? false}
teamIdForApi={teamIdForApi}
/>
{renderVulnTables()}
{/* at this point, error can only be 403 per above handling */}
{isVulnError ? (
<DetailsNoHosts
header="Vulnerability not detected"
details={`No hosts ${
teamIdForApi ? "on this team " : ""
}are affected by this CVE.`}
/>
) : (
renderCards(vuln)
)}
</>
);
};