mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
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:
parent
9ed0c193c8
commit
436e900d62
4 changed files with 156 additions and 143 deletions
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue