From 35a467b7e0c10c6d6482dc6a87fc6ca2ce24419e Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:12:56 -0400 Subject: [PATCH] Fleet UI: Refactor client filtered counts for cleaner rendering (#19689) --- changes/19090-flashing-count | 1 + .../LiveQuery/TargetsInput/TargetsInput.tsx | 2 - .../TableContainer/DataTable/DataTable.tsx | 4 +- .../TableContainer/TableContainer.tsx | 62 ++++-------------- .../TableContainer/TableCount/TableCount.tsx | 14 +++++ .../TableContainer/TableCount/index.ts | 1 + .../components/TableContainer/_styles.scss | 4 +- .../utilities/TableContainerUtils.ts | 7 +-- .../SoftwareOSTable/SoftwareOSTable.tsx | 36 ++++------- .../SoftwareOS/SoftwareOSTable/_styles.scss | 6 -- .../SoftwareTable/SoftwareTable.tsx | 36 ++++------- .../SoftwareTitles/SoftwareTable/_styles.scss | 7 --- .../SoftwareVulnerabilitiesTable.tsx | 36 ++++------- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 17 ++--- .../pages/hosts/ManageHostsPage/_styles.scss | 10 +-- .../HostQueryReport/HQRTable/HQRTable.tsx | 8 +-- .../HostQueryReport/HQRTable/_styles.scss | 24 ++----- .../HostSoftwareTable/HostSoftwareTable.tsx | 15 +---- .../pages/hosts/details/cards/Users/Users.tsx | 10 ++- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 37 +++++------ .../components/PolicyResults/_styles.scss | 10 --- .../ManageQueriesPage/ManageQueriesPage.tsx | 2 +- .../components/QueriesTable/QueriesTable.tsx | 63 +++++++++++++++++-- .../components/QueryReport/QueryReport.tsx | 15 +++-- .../edit/components/QueryResults/_styles.scss | 15 ----- 25 files changed, 173 insertions(+), 269 deletions(-) create mode 100644 changes/19090-flashing-count create mode 100644 frontend/components/TableContainer/TableCount/TableCount.tsx create mode 100644 frontend/components/TableContainer/TableCount/index.ts diff --git a/changes/19090-flashing-count b/changes/19090-flashing-count new file mode 100644 index 0000000000..55c4abe22e --- /dev/null +++ b/changes/19090-flashing-count @@ -0,0 +1 @@ +- Cleanup count rendering fixing clientside flashing counts diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx index c3f2dd5254..e18200309c 100644 --- a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx +++ b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx @@ -76,7 +76,6 @@ const TargetsInput = ({ columnConfigs={searchResultsTableConfig} data={dropdownHosts} isLoading={isTargetsLoading} - resultsTitle="" emptyComponent={() => (
@@ -107,7 +106,6 @@ const TargetsInput = ({ columnConfigs={selectedHostsTableConifg} data={targetedHosts} isLoading={false} - resultsTitle="" showMarkAllPages={false} isAllPagesSelected={false} disableCount diff --git a/frontend/components/TableContainer/DataTable/DataTable.tsx b/frontend/components/TableContainer/DataTable/DataTable.tsx index 62d3b3c378..8f8906ee1c 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tsx @@ -43,7 +43,7 @@ interface IDataTableProps { showMarkAllPages: boolean; isAllPagesSelected: boolean; // TODO: make dependent on showMarkAllPages toggleAllPagesSelected?: any; // TODO: an event type and make it dependent on showMarkAllPages - resultsTitle: string; + resultsTitle?: string; defaultPageSize: number; defaultPageIndex?: number; primarySelectAction?: IActionButtonProps; @@ -85,7 +85,7 @@ const DataTable = ({ showMarkAllPages, isAllPagesSelected, toggleAllPagesSelected, - resultsTitle, + resultsTitle = "results", defaultPageSize, defaultPageIndex, primarySelectAction, diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 15e1c290c5..3b68cbb470 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -12,7 +12,6 @@ import Icon from "components/Icon/Icon"; import { COLORS } from "styles/var/colors"; import DataTable from "./DataTable/DataTable"; -import TableContainerUtils from "./utilities/TableContainerUtils"; import { IActionButtonProps } from "./DataTable/ActionButton/ActionButton"; export interface ITableQueryData { @@ -44,7 +43,8 @@ interface ITableContainerProps { inputPlaceHolder?: string; disableActionButton?: boolean; disableMultiRowSelect?: boolean; - resultsTitle: string; + /** resultsTitle used in DataTable for matching results text */ + resultsTitle?: string; resultsHtml?: JSX.Element; additionalQueries?: string; emptyComponent: React.ElementType; @@ -64,10 +64,6 @@ interface ITableContainerProps { primarySelectAction?: IActionButtonProps; /** Secondary button/s after selecting a row */ secondarySelectActions?: IActionButtonProps[]; // TODO: Combine with primarySelectAction as these are all rendered in the same spot - /** - * @deprecated please use renderCount instead - * */ - filteredCount?: number; searchToolTipText?: string; // TODO - consolidate this functionality within `filters` searchQueryColumn?: string; @@ -103,7 +99,6 @@ interface ITableContainerProps { * bar and API call so TableContainer will reset its page state to 0 */ resetPageIndex?: boolean; disableTableHeader?: boolean; - show0Count?: boolean; } const baseClass = "table-container"; @@ -140,7 +135,6 @@ const TableContainer = ({ disableCount, primarySelectAction, secondarySelectActions, - filteredCount, searchToolTipText, isClientSidePagination, onClientSidePaginationChange, @@ -160,7 +154,6 @@ const TableContainer = ({ setExportRows, resetPageIndex, disableTableHeader, - show0Count, }: ITableContainerProps) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const [sortHeader, setSortHeader] = useState(defaultSortHeader || ""); @@ -252,16 +245,6 @@ const TableContainer = ({ additionalQueries, ]); - // TODO: refactor existing components relying on displayCount to use renderCount pattern - const displayCount = useCallback((): any => { - if (typeof filteredCount === "number") { - return filteredCount; - } else if (typeof clientFilterCount === "number") { - return clientFilterCount; - } - return data?.length || 0; - }, [filteredCount, clientFilterCount, data]); - const renderPagination = useCallback(() => { if (disablePagination || isClientSidePagination) { return null; @@ -309,37 +292,16 @@ const TableContainer = ({ stackControls ? "stack-table-controls" : "" }`} > - - {renderCount && ( -
- {renderCount()} -
- )} - {!renderCount && - !disableCount && - (isMultiColumnFilter || displayCount() || show0Count) ? ( -
- {TableContainerUtils.generateResultsCountText( - resultsTitle, - displayCount(), - show0Count - )} - {resultsHtml} -
- ) : ( -
- )} - + {renderCount && !disableCount && ( +
+ {renderCount()} +
+ )} {actionButton && !actionButton.hideButton && ( )} -
+ ); }, [isLoadingHostsCount, hostsCount]); diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index b052b1f504..7053e754a1 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -273,14 +273,6 @@ } &__export-btn { - margin-left: $pad-medium; - - img { - width: 13px; - height: 13px; - margin-left: 8px; - position: relative; - top: -2px; - } + margin-left: $pad-xsmall; } } diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx index 30969112a1..eb957762ee 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx @@ -2,6 +2,7 @@ import Button from "components/buttons/Button"; import EmptyTable from "components/EmptyTable"; import Icon from "components/Icon"; import TableContainer from "components/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; import React, { useCallback, useState } from "react"; import { Row } from "react-table"; import { @@ -119,15 +120,14 @@ const HQRTable = ({ }, [lastFetched, hostName, reportClipped]); const renderCount = useCallback(() => { - const count = filteredResults.length; return ( -
- {`${count} result${count === 1 ? "" : "s"}`} + <> + Last fetched{" "} -
+ ); }, [filteredResults.length, lastFetched]); diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss b/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss index 9fb768a1ee..917d35355b 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss @@ -1,30 +1,14 @@ .hqr-table { gap: $pad-medium; - &__results-count-and-last-fetched { - display: flex; - align-items: baseline; - gap: $pad-small; - .last-fetched { - font-weight: initial; - @include grey-text; - } + .last-fetched { + font-weight: initial; + @include grey-text; } + &__results-cta { display: flex; gap: $pad-medium; - .button { - height: auto; - } - } - - &__export-btn { - .children-wrapper { - align-self: flex-end; - } - .icon { - display: initial; - } } &__query-info { diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx index 91a504ba66..b450ec8fa9 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx @@ -7,8 +7,10 @@ import { getNextLocationPath } from "utilities/helpers"; import TableContainer from "components/TableContainer"; import { ITableQueryData } from "components/TableContainer/TableContainer"; +import { generateResultsCountText } from "components/TableContainer/utilities/TableContainerUtils"; import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable"; +import TableCount from "components/TableContainer/TableCount"; const DEFAULT_PAGE_SIZE = 20; @@ -26,16 +28,6 @@ interface IHostSoftwareTableProps { pagePath: string; } -const SoftwareCount = ({ count }: { count: number }) => { - return ( -
- - {count === 1 ? `${count} software item` : `${count} software items`} - -
- ); -}; - const HostSoftwareTable = ({ tableConfig, data, @@ -108,7 +100,7 @@ const HostSoftwareTable = ({ const memoizedSoftwareCount = useCallback(() => { const count = data?.count || data?.software.length || 0; - return ; + return ; }, [data?.count, data?.software.length]); const memoizedEmptyComponent = useCallback(() => { @@ -119,7 +111,6 @@ const HostSoftwareTable = ({
{ const tableHeaders = generateUsersTableHeaders(); + const renderUsersCount = useCallback(() => { + return ; + }, [usersState.length]); + if (!hostUsersEnabled) { return ( ( ) : ( diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index ba2b5a2eac..bc0d9765b7 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -34,6 +34,7 @@ import teamPoliciesAPI, { import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; import { ITableQueryData } from "components/TableContainer/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; import Button from "components/buttons/Button"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; @@ -624,19 +625,21 @@ const ManagePolicyPage = ({ } const renderPoliciesCount = (count?: number) => { - // Show count if there is no errors AND there are policy results or a search filter - const showCount = - count !== undefined && - !policiesErrors && - (policyResults || searchQuery !== ""); + // Hide count if fetching count || there are errors OR there are no policy results with no a search filter + const isFetchingCount = isAnyTeamSelected + ? isFetchingTeamCountMergeInherited + : isFetchingGlobalCount; - return ( -
- {showCount && ( - {`${count} polic${count === 1 ? "y" : "ies"}`} - )} -
- ); + const hideCount = + isFetchingCount || + policiesErrors || + (!policyResults && searchQuery === ""); + + if (hideCount) { + return null; + } + + return ; }; const renderMainTable = () => { @@ -658,9 +661,7 @@ const ManagePolicyPage = ({ currentTeam={currentTeamSummary} currentAutomatedPolicies={currentAutomatedPolicies} renderPoliciesCount={() => - (!isFetchingTeamCountMergeInherited && - renderPoliciesCount(teamPoliciesCountMergeInherited)) || - null + renderPoliciesCount(teamPoliciesCountMergeInherited) } isPremiumTier={isPremiumTier} searchQuery={searchQuery} @@ -683,11 +684,7 @@ const ManagePolicyPage = ({ currentTeam={currentTeamSummary} currentAutomatedPolicies={currentAutomatedPolicies} isPremiumTier={isPremiumTier} - renderPoliciesCount={() => - (!isFetchingGlobalCount && - renderPoliciesCount(globalPoliciesCount)) || - null - } + renderPoliciesCount={() => renderPoliciesCount(globalPoliciesCount)} searchQuery={searchQuery} sortHeader={sortHeader} sortDirection={sortDirection} diff --git a/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss index 045ce4b631..87e8aba720 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss +++ b/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss @@ -3,16 +3,6 @@ margin: 2rem auto 1.25rem; } - &__export-btn { - img { - width: 13px; - height: 13px; - margin-left: 8px; - position: relative; - bottom: 2px; - } - } - .data-table__wrapper { overflow-x: scroll; } diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index d098dec5cb..b8b850673f 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -43,7 +43,7 @@ interface IManageQueriesPageProps { location: { pathname: string; query: { - platform?: string; + platform?: SupportedPlatform; page?: string; query?: string; order_key?: string; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index f25578beec..dc1b754d8e 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -1,16 +1,25 @@ /* eslint-disable react/prop-types */ -import React, { useContext, useCallback, useMemo } from "react"; +import React, { + useContext, + useCallback, + useMemo, + useState, + useEffect, +} from "react"; import { InjectedRouter } from "react-router"; import { AppContext } from "context/app"; import { IEmptyTableProps } from "interfaces/empty_table"; +import { SupportedPlatform } from "interfaces/platform"; import { IEnhancedQuery } from "interfaces/schedulable_query"; import { ITableQueryData } from "components/TableContainer/TableContainer"; import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton/ActionButton"; import PATHS from "router/paths"; import { getNextLocationPath } from "utilities/helpers"; +import { checkPlatformCompatibility } from "utilities/sql_tools"; import Button from "components/buttons/Button"; import TableContainer from "components/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; import CustomLink from "components/CustomLink"; import EmptyTable from "components/EmptyTable"; // @ts-ignore @@ -29,7 +38,7 @@ export interface IQueriesTableProps { isAnyTeamObserverPlus: boolean; router?: InjectedRouter; queryParams?: { - platform?: string; + platform?: SupportedPlatform; page?: string; query?: string; order_key?: string; @@ -92,6 +101,38 @@ const QueriesTable = ({ }: IQueriesTableProps): JSX.Element | null => { const { currentUser } = useContext(AppContext); + // Client side filtering bugs fixed with bypassing TableContainer filters + // queriesState tracks search filter and compatible platform filter + // to correctly show filtered queries and filtered count + // isQueryStateLoading prevents flashing of unfiltered count during clientside filtering + const [queriesState, setQueriesState] = useState([]); + const [isQueriesStateLoading, setIsQueriesStateLoading] = useState(true); + + useEffect(() => { + setIsQueriesStateLoading(true); + if (queriesList) { + setQueriesState( + queriesList.filter((query) => { + const filterSearchQuery = queryParams?.query + ? query.name + .toLowerCase() + .includes(queryParams?.query.toLowerCase()) + : true; + + const compatiblePlatforms = + checkPlatformCompatibility(query.query).platforms || []; + + const filterCompatiblePlatform = queryParams?.platform + ? compatiblePlatforms.includes(queryParams?.platform) + : true; + + return filterSearchQuery && filterCompatiblePlatform; + }) || [] + ); + } + setIsQueriesStateLoading(false); + }, [queriesList, queryParams?.query]); + // Functions to avoid race conditions const initialSearchQuery = (() => queryParams?.query ?? "")(); const initialSortHeader = (() => @@ -236,6 +277,15 @@ const QueriesTable = ({ ); }, [platform, queryParams, router]); + const renderQueriesCount = useCallback(() => { + // Fixes flashing incorrect count before clientside filtering + if (isQueriesStateLoading) { + return null; + } + + return ; + }, [queriesState, isQueriesStateLoading]); + const columnConfigs = useMemo( () => currentUser && @@ -281,14 +331,15 @@ const QueriesTable = ({ } as IActionButtonProps), [onDeleteQueryClick] ); + return columnConfigs && !isLoading ? (
) : ( diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx index 7ccd7fb166..9bebede919 100644 --- a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx +++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx @@ -14,6 +14,8 @@ import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; import TableContainer from "components/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; +import { generateResultsCountText } from "components/TableContainer/utilities/TableContainerUtils"; import TooltipWrapper from "components/TooltipWrapper"; import EmptyTable from "components/EmptyTable"; @@ -102,7 +104,7 @@ const QueryReport = ({ if (isClipped) { return ( -
+ <> @@ -115,16 +117,13 @@ const QueryReport = ({ } > - {`${count} result${count === 1 ? "" : "s"}`} + {generateResultsCountText("results", count)} -
+ ); } - return ( -
- {`${count} result${count === 1 ? "" : "s"}`} -
- ); + + return ; }, [filteredResults.length, isClipped]); const renderTable = () => { diff --git a/frontend/pages/queries/edit/components/QueryResults/_styles.scss b/frontend/pages/queries/edit/components/QueryResults/_styles.scss index 36ea6b4290..7b7fe41c57 100644 --- a/frontend/pages/queries/edit/components/QueryResults/_styles.scss +++ b/frontend/pages/queries/edit/components/QueryResults/_styles.scss @@ -8,21 +8,6 @@ margin-right: $pad-medium; } - &__export-btn { - img { - width: 13px; - margin-left: 8px; - position: relative; - bottom: 2px; - } - } - &__show-query-btn { - img { - width: 13px; - margin-left: 8px; - } - } - .data-table__wrapper { overflow-x: scroll; }