diff --git a/frontend/components/Pagination/Pagination.jsx b/frontend/components/Pagination/Pagination.jsx index 7926a01896..e99348c102 100644 --- a/frontend/components/Pagination/Pagination.jsx +++ b/frontend/components/Pagination/Pagination.jsx @@ -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 ); }; diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index f7aaf8afc9..c910fcf9a3 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -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, diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index c3d6a8b9a8..f4bb9827a7 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -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; diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx index 5fc34dd638..5d38175056 100644 --- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx +++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx @@ -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(); 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( [ "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( [ "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 (