Refine manage software page features (#4040)
This commit is contained in:
@@ -12,6 +12,7 @@ class Pagination extends PureComponent {
|
||||
resultsPerPage: PropTypes.number,
|
||||
onPaginationChange: PropTypes.func,
|
||||
resultsOnCurrentPage: PropTypes.number,
|
||||
disableNextPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
disablePrev = () => {
|
||||
@@ -23,7 +24,8 @@ class Pagination extends PureComponent {
|
||||
// but this seems to work when there is no data in the table.
|
||||
return (
|
||||
this.props.resultsOnCurrentPage === undefined ||
|
||||
this.props.resultsOnCurrentPage < this.props.resultsPerPage
|
||||
this.props.resultsOnCurrentPage < this.props.resultsPerPage ||
|
||||
this.props.disableNextPage
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ interface ITableContainerProps {
|
||||
searchable?: boolean;
|
||||
wideSearch?: boolean;
|
||||
disablePagination?: boolean;
|
||||
disableNextPage?: boolean; // disableNextPage is a temporary workaround for the case
|
||||
// where the number of items on the last page is equal to the page size.
|
||||
// The old page controls for server-side pagination render a no results screen
|
||||
// with a back button. This fix instead disables the next button in that case.
|
||||
disableCount?: boolean;
|
||||
primarySelectActionButtonVariant?: ButtonVariant;
|
||||
primarySelectActionButtonIcon?: string;
|
||||
@@ -105,6 +109,7 @@ const TableContainer = ({
|
||||
searchable,
|
||||
wideSearch,
|
||||
disablePagination,
|
||||
disableNextPage,
|
||||
disableCount,
|
||||
primarySelectActionButtonVariant = "brand",
|
||||
primarySelectActionButtonIcon,
|
||||
@@ -159,10 +164,13 @@ const TableContainer = ({
|
||||
};
|
||||
|
||||
const hasPageIndexChangedRef = useRef(false);
|
||||
const onPaginationChange = (newPage: number) => {
|
||||
setPageIndex(newPage);
|
||||
hasPageIndexChangedRef.current = true;
|
||||
};
|
||||
const onPaginationChange = useCallback(
|
||||
(newPage: number) => {
|
||||
setPageIndex(newPage);
|
||||
hasPageIndexChangedRef.current = true;
|
||||
},
|
||||
[hasPageIndexChangedRef]
|
||||
);
|
||||
|
||||
const onResultsCountChange = (resultsCount: number) => {
|
||||
setClientFilterCount(resultsCount);
|
||||
@@ -217,12 +225,14 @@ const TableContainer = ({
|
||||
currentPage={pageIndex}
|
||||
resultsPerPage={pageSize}
|
||||
onPaginationChange={onPaginationChange}
|
||||
disableNextPage={disableNextPage}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
data,
|
||||
disablePagination,
|
||||
isClientSidePagination,
|
||||
data,
|
||||
disableNextPage,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
onPaginationChange,
|
||||
|
||||
@@ -214,6 +214,10 @@ export interface IConfigNested {
|
||||
host_expiry_enabled: boolean;
|
||||
host_expiry_window: number;
|
||||
};
|
||||
host_settings: {
|
||||
enable_host_users: boolean;
|
||||
enable_software_inventory: boolean;
|
||||
};
|
||||
agent_options: string;
|
||||
update_interval: {
|
||||
osquery_detail: number;
|
||||
@@ -226,9 +230,18 @@ export interface IConfigNested {
|
||||
expiration: string;
|
||||
note: string;
|
||||
};
|
||||
vulnerability_settings: {
|
||||
vulnerabilities: {
|
||||
databases_path: string;
|
||||
periodicity: number;
|
||||
cpe_database_url: string;
|
||||
cve_feed_prefix_url: string;
|
||||
current_instance_checks: string;
|
||||
disable_data_sync: boolean;
|
||||
};
|
||||
// Note: `vulnerability_settings` is deprecated and should not be used
|
||||
// vulnerability_settings: {
|
||||
// databases_path: string;
|
||||
// };
|
||||
webhook_settings: {
|
||||
host_status_webhook: IWebhookHostStatus;
|
||||
failing_policies_webhook: IWebhookFailingPolicies;
|
||||
|
||||
@@ -14,7 +14,6 @@ import { getConfig } from "redux/nodes/app/actions";
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
import configAPI from "services/entities/config";
|
||||
import usersAPI, { IGetMeResponse } from "services/entities/users";
|
||||
import softwareAPI, {
|
||||
ISoftwareResponse,
|
||||
ISoftwareCountResponse,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import Spinner from "components/Spinner";
|
||||
import TableContainer, { ITableQueryData } from "components/TableContainer";
|
||||
import TableDataError from "components/TableDataError";
|
||||
@@ -37,7 +37,7 @@ import TeamsDropdownHeader, {
|
||||
import ExternalLinkIcon from "../../../../assets/images/open-new-tab-12x12@2x.png";
|
||||
import QuestionIcon from "../../../../assets/images/icon-question-16x16@2x.png";
|
||||
|
||||
import generateTableHeaders from "./SoftwareTableConfig";
|
||||
import softwareTableHeaders from "./SoftwareTableConfig";
|
||||
import ManageAutomationsModal from "./components/ManageAutomationsModal";
|
||||
import EmptySoftware from "../components/EmptySoftware";
|
||||
|
||||
@@ -49,6 +49,9 @@ interface IManageSoftwarePageProps {
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
interface IHeaderButtonsState extends ITeamsDropdownState {
|
||||
isLoading: boolean;
|
||||
}
|
||||
const DEFAULT_SORT_DIRECTION = "desc";
|
||||
const DEFAULT_SORT_HEADER = "hosts_count";
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -71,8 +74,7 @@ const ManageSoftwarePage = ({
|
||||
isGlobalMaintainer,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const [isLoadingSoftware, setIsLoadingSoftware] = useState(true);
|
||||
const [isLoadingCount, setIsLoadingCount] = useState(true);
|
||||
const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
|
||||
const [filterVuln, setFilterVuln] = useState(
|
||||
location?.query?.vulnerable || false
|
||||
);
|
||||
@@ -92,36 +94,36 @@ const ManageSoftwarePage = ({
|
||||
setFilterVuln(!!location.query.vulnerable);
|
||||
}, [location]);
|
||||
|
||||
// TODO: combine string and object so only one array element in query key; figure out typing and
|
||||
// destructuring for queryfn
|
||||
|
||||
useQuery(["me"], () => usersAPI.me(), {
|
||||
onSuccess: ({ user, available_teams }: IGetMeResponse) => {
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(available_teams);
|
||||
const { data: config } = useQuery(["config"], configAPI.loadAll, {
|
||||
onSuccess: (data) => {
|
||||
setIsSoftwareEnabled(data?.host_settings?.enable_software_inventory);
|
||||
},
|
||||
});
|
||||
|
||||
const { data: software, error: softwareError } = useQuery<
|
||||
ISoftwareResponse,
|
||||
Error
|
||||
>(
|
||||
const {
|
||||
data: software,
|
||||
error: softwareError,
|
||||
isFetching: isFetchingSoftware,
|
||||
} = useQuery<ISoftwareResponse, Error>(
|
||||
[
|
||||
"software",
|
||||
{
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
searchQuery,
|
||||
sortDirection,
|
||||
sortHeader,
|
||||
teamId: currentTeam?.id,
|
||||
vulnerable: !!location.query.vulnerable,
|
||||
urlPath: location.pathname,
|
||||
urlQueryString: location.search,
|
||||
params: {
|
||||
scope: "software",
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
searchQuery,
|
||||
sortDirection,
|
||||
sortHeader,
|
||||
teamId: currentTeam?.id,
|
||||
vulnerable: !!location.query.vulnerable,
|
||||
},
|
||||
},
|
||||
location.pathname,
|
||||
location.search,
|
||||
],
|
||||
// TODO: figure out typing and destructuring for query key inside query function
|
||||
() => {
|
||||
setIsLoadingSoftware(true);
|
||||
const params = {
|
||||
page: pageIndex,
|
||||
perPage: PAGE_SIZE,
|
||||
@@ -134,35 +136,27 @@ const ManageSoftwarePage = ({
|
||||
return softwareAPI.load(params);
|
||||
},
|
||||
{
|
||||
// If keepPreviousData is enabled,
|
||||
// useQuery no longer returns isLoading when making new calls after load
|
||||
// So we manage our own load states
|
||||
keepPreviousData: true,
|
||||
staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
|
||||
onSuccess: () => {
|
||||
setIsLoadingSoftware(false);
|
||||
},
|
||||
onError: () => {
|
||||
setIsLoadingSoftware(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { data: softwareCount, error: softwareCountError } = useQuery<
|
||||
ISoftwareCountResponse,
|
||||
Error,
|
||||
number
|
||||
>(
|
||||
const {
|
||||
data: softwareCount,
|
||||
error: softwareCountError,
|
||||
isFetching: isFetchingCount,
|
||||
} = useQuery<ISoftwareCountResponse, Error, number>(
|
||||
[
|
||||
"softwareCount",
|
||||
{
|
||||
searchQuery,
|
||||
vulnerable: !!location.query.vulnerable,
|
||||
teamId: currentTeam?.id,
|
||||
params: {
|
||||
searchQuery,
|
||||
vulnerable: !!location.query.vulnerable,
|
||||
teamId: currentTeam?.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
() => {
|
||||
setIsLoadingCount(true);
|
||||
return softwareAPI.count({
|
||||
query: searchQuery,
|
||||
vulnerable: !!location.query.vulnerable,
|
||||
@@ -175,13 +169,6 @@ const ManageSoftwarePage = ({
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
select: (data) => data.count,
|
||||
onSuccess: () => {
|
||||
setIsLoadingCount(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.log("useQuery error: ", err);
|
||||
setIsLoadingCount(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -277,12 +264,12 @@ const ManageSoftwarePage = ({
|
||||
};
|
||||
|
||||
const renderHeaderButtons = (
|
||||
state: ITeamsDropdownState
|
||||
state: IHeaderButtonsState
|
||||
): JSX.Element | null => {
|
||||
if (
|
||||
canAddOrRemoveSoftwareWebhook &&
|
||||
(!isPremiumTier || state.teamId === 0) &&
|
||||
!isLoadingSoftwareVulnerabilitiesWebhook
|
||||
(state.isGlobalAdmin || state.isGlobalMaintainer) &&
|
||||
(!state.isPremiumTier || state.teamId === 0) &&
|
||||
!state.isLoading
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
@@ -301,8 +288,8 @@ const ManageSoftwarePage = ({
|
||||
return (
|
||||
<p>
|
||||
Search for installed software{" "}
|
||||
{canAddOrRemoveSoftwareWebhook &&
|
||||
(!isPremiumTier || state.teamId === 0) &&
|
||||
{(state.isGlobalAdmin || state.isGlobalMaintainer) &&
|
||||
(!state.isPremiumTier || state.teamId === 0) &&
|
||||
"and manage automations for detected vulnerabilities (CVEs)"}{" "}
|
||||
on{" "}
|
||||
<b>
|
||||
@@ -324,23 +311,29 @@ const ManageSoftwarePage = ({
|
||||
onChange={onTeamSelect}
|
||||
defaultTitle="Software"
|
||||
description={renderHeaderDescription}
|
||||
buttons={renderHeaderButtons}
|
||||
buttons={(state) =>
|
||||
renderHeaderButtons({
|
||||
...state,
|
||||
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]);
|
||||
|
||||
const renderSoftwareCount = useCallback(() => {
|
||||
const count = softwareCount;
|
||||
let lastUpdatedAt = software?.counts_updated_at;
|
||||
if (!lastUpdatedAt || lastUpdatedAt === "0001-01-01T00:00:00Z") {
|
||||
lastUpdatedAt = "never";
|
||||
} else {
|
||||
lastUpdatedAt = formatDistanceToNowStrict(new Date(lastUpdatedAt), {
|
||||
addSuffix: true,
|
||||
});
|
||||
const lastUpdatedAt = software?.counts_updated_at
|
||||
? formatDistanceToNowStrict(new Date(software?.counts_updated_at), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: software?.counts_updated_at;
|
||||
|
||||
if (!isSoftwareEnabled || !lastUpdatedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (softwareCountError && !isLoadingCount) {
|
||||
if (softwareCountError && !isFetchingCount) {
|
||||
return (
|
||||
<span className={`${baseClass}__count count-error`}>
|
||||
Failed to load software count
|
||||
@@ -352,7 +345,7 @@ const ManageSoftwarePage = ({
|
||||
return count !== undefined ? (
|
||||
<span
|
||||
className={`${baseClass}__count ${
|
||||
isLoadingCount ? "count-loading" : ""
|
||||
isFetchingCount ? "count-loading" : ""
|
||||
}`}
|
||||
>
|
||||
{`${count} software item${count === 1 ? "" : "s"}`}
|
||||
@@ -387,7 +380,7 @@ const ManageSoftwarePage = ({
|
||||
</span>
|
||||
</span>
|
||||
) : null;
|
||||
}, [isLoadingCount, software, softwareCountError, softwareCount]);
|
||||
}, [isFetchingCount, software, softwareCountError, softwareCount]);
|
||||
|
||||
// TODO: retool this with react-router location descriptor objects
|
||||
const buildUrlQueryString = (queryString: string, vulnerable: boolean) => {
|
||||
@@ -456,24 +449,38 @@ const ManageSoftwarePage = ({
|
||||
);
|
||||
};
|
||||
|
||||
return !availableTeams ? (
|
||||
// TODO: Rework after backend is adjusted to differentiate empty search/filter results from
|
||||
// collecting inventory
|
||||
const isCollectingInventory =
|
||||
!searchQuery &&
|
||||
!filterVuln &&
|
||||
!currentTeam?.id &&
|
||||
!pageIndex &&
|
||||
!software?.software &&
|
||||
software?.counts_updated_at === null;
|
||||
|
||||
const isLastPage =
|
||||
!!softwareCount &&
|
||||
PAGE_SIZE * pageIndex + (software?.software?.length || 0) >= softwareCount;
|
||||
|
||||
return !availableTeams || !config ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||
{renderHeader()}
|
||||
{softwareError && !isLoadingSoftware ? (
|
||||
{softwareError && !isFetchingSoftware ? (
|
||||
<TableDataError />
|
||||
) : (
|
||||
<TableContainer
|
||||
columns={generateTableHeaders()}
|
||||
data={software?.software || []}
|
||||
isLoading={isLoadingSoftware}
|
||||
columns={softwareTableHeaders}
|
||||
data={(isSoftwareEnabled && software?.software) || []}
|
||||
isLoading={isFetchingSoftware || isFetchingCount}
|
||||
resultsTitle={"software items"}
|
||||
emptyComponent={() =>
|
||||
EmptySoftware(
|
||||
(filterVuln && "vulnerable") ||
|
||||
(searchQuery && "search") ||
|
||||
(!isSoftwareEnabled && "disabled") ||
|
||||
(isCollectingInventory && "collecting") ||
|
||||
"default"
|
||||
)
|
||||
}
|
||||
@@ -483,6 +490,7 @@ const ManageSoftwarePage = ({
|
||||
pageSize={PAGE_SIZE}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disableNextPage={isLastPage}
|
||||
searchable
|
||||
inputPlaceHolder="Search software by name or vulnerabilities (CVEs)"
|
||||
onQueryChange={onQueryChange}
|
||||
|
||||
@@ -14,12 +14,29 @@ import Chevron from "../../../../assets/images/icon-chevron-right-blue-16x16@2x.
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
value: any;
|
||||
value: number | string | IVulnerability[];
|
||||
};
|
||||
row: {
|
||||
original: ISoftware;
|
||||
};
|
||||
}
|
||||
interface IStringCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface INumberCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IVulnCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: IVulnerability[];
|
||||
};
|
||||
}
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
title: string;
|
||||
@@ -36,28 +53,54 @@ interface IDataColumn {
|
||||
disableSortBy?: boolean;
|
||||
}
|
||||
|
||||
const condense = (vulnerabilities: IVulnerability[]) => {
|
||||
const condensed =
|
||||
(vulnerabilities?.length &&
|
||||
vulnerabilities
|
||||
.slice(-3)
|
||||
.map((v) => v.cve)
|
||||
.reverse()) ||
|
||||
[];
|
||||
return vulnerabilities.length > 3
|
||||
? condensed.concat(`+${vulnerabilities.length - 3} more`)
|
||||
: condensed;
|
||||
};
|
||||
|
||||
const softwareTableHeaders = [
|
||||
{
|
||||
title: "Name",
|
||||
Header: "Name",
|
||||
disableSortBy: true,
|
||||
accessor: "name",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps: IStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
accessor: "version",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps: IStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Vulnerabilities",
|
||||
Header: "Vulnerabilities",
|
||||
disableSortBy: true,
|
||||
accessor: "vulnerabilities",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
const vulnerabilities: IVulnerability[] = cellProps.cell.value;
|
||||
Cell: (cellProps: IVulnCellProps) => {
|
||||
const vulnerabilities = cellProps.cell.value || [];
|
||||
const tooltipText = condense(vulnerabilities)?.map((value) => {
|
||||
return (
|
||||
<span key={`vuln_${value}`}>
|
||||
{value}
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
if (!vulnerabilities?.length) {
|
||||
return <span className="vulnerabilities text-muted">---</span>;
|
||||
}
|
||||
@@ -84,12 +127,7 @@ const softwareTableHeaders = [
|
||||
data-html
|
||||
>
|
||||
<span className={`vulnerabilities tooltip__tooltip-text`}>
|
||||
{vulnerabilities.map((v) => (
|
||||
<span key={v.cve}>
|
||||
{v.cve}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
{tooltipText}
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
@@ -106,14 +144,16 @@ const softwareTableHeaders = [
|
||||
),
|
||||
disableSortBy: false,
|
||||
accessor: "hosts_count",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps: INumberCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
Header: "",
|
||||
disableSortBy: true,
|
||||
accessor: "id",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
Cell: (cellProps: INumberCellProps) => {
|
||||
return (
|
||||
<Link
|
||||
to={`${PATHS.MANAGE_HOSTS}?software_id=${cellProps.cell.value}`}
|
||||
@@ -127,8 +167,4 @@ const softwareTableHeaders = [
|
||||
},
|
||||
];
|
||||
|
||||
const generateTableHeaders = (): IDataColumn[] => {
|
||||
return softwareTableHeaders;
|
||||
};
|
||||
|
||||
export default generateTableHeaders;
|
||||
export default softwareTableHeaders;
|
||||
|
||||
@@ -120,26 +120,41 @@
|
||||
display: none;
|
||||
}
|
||||
&__empty-software {
|
||||
margin: $pad-large auto 0;
|
||||
margin: 80px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
.empty-software__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
p {
|
||||
color: $core-fleet-black;
|
||||
font-weight: $regular;
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $core-vibrant-blue;
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
p {
|
||||
color: $core-fleet-black;
|
||||
font-weight: $regular;
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $core-vibrant-blue;
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__count {
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
import React from "react";
|
||||
|
||||
import ExternalLinkIcon from "../../../../assets/images/open-new-tab-12x12@2x.png";
|
||||
|
||||
const baseClass = "manage-software-page";
|
||||
|
||||
type IEmptySoftware = "search" | "vulnerable" | "default" | "";
|
||||
type IEmptySoftware = "disabled" | "collecting" | "default" | "";
|
||||
|
||||
const EmptySoftware = (message: IEmptySoftware): JSX.Element => {
|
||||
switch (message) {
|
||||
case "search":
|
||||
case "disabled": {
|
||||
return (
|
||||
<div className={`${baseClass}__empty-software`}>
|
||||
<h1>No software matches the current search criteria.</h1>
|
||||
<p>
|
||||
Expecting to see software? Try again in a few seconds as the system
|
||||
catches up.
|
||||
</p>
|
||||
<div className="empty-software__inner">
|
||||
<h1>Software inventory is disabled.</h1>
|
||||
<p>
|
||||
Check out the Fleet documentation on{" "}
|
||||
<a
|
||||
href="https://fleetdm.com/docs/using-fleet/vulnerability-processing#configuration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
how to configure software inventory{" "}
|
||||
<img alt="External link" src={ExternalLinkIcon} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "vulnerable":
|
||||
default:
|
||||
}
|
||||
case "collecting": {
|
||||
return (
|
||||
<div className={`${baseClass}__empty-software`}>
|
||||
<h1>
|
||||
No installed software{" "}
|
||||
{message === "vulnerable"
|
||||
? "with detected vulnerabilities"
|
||||
: "detected"}
|
||||
.
|
||||
</h1>
|
||||
<p>
|
||||
Expecting to see software? Check out the Fleet documentation on{" "}
|
||||
<a
|
||||
href="https://fleetdm.com/docs/deploying/configuration#software-inventory"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
how to configure software inventory
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="empty-software__inner">
|
||||
<h1>Fleet is collecting software inventory.</h1>
|
||||
<p>Try again in about 1 hour as the system catches up.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<div className={`${baseClass}__empty-software`}>
|
||||
<div className="empty-software__inner">
|
||||
<h1>No software matches the current search criteria.</h1>
|
||||
<p>
|
||||
Expecting to see software? Try again in about 1 hour as the system
|
||||
catches up.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import { IConfigNested } from "interfaces/config";
|
||||
|
||||
// TODO: add other methods from "fleet/entities/config"
|
||||
|
||||
export default {
|
||||
loadAll: () => {
|
||||
loadAll: (): Promise<IConfigNested> => {
|
||||
const { CONFIG } = endpoints;
|
||||
const path = `${CONFIG}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user