From d49255dfdbb91b2ea88263d30270d399a303e065 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:23:07 -0800 Subject: [PATCH] UI - clip live query results (#15760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #14874 - once 250,000 (results + errors) return, end the query campaign and display the "clipped" banner - Format host counts with commas - misc. cleanup Screenshot 2023-12-19 at 4 37 28 PM Screenshot 2023-12-19 at 4 37 02 PM ## Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/14874-clip-live-query-results | 1 + .../components/LiveQuery/SelectTargets.tsx | 4 +-- .../LiveQuery/TargetsInput/TargetsInput.tsx | 2 -- .../TableContainer/TableContainerUtils.ts | 2 +- .../QueryResultsHeading.tsx | 2 +- .../components/QueryResults/QueryResults.tsx | 26 +++++++++++++++- .../live/LiveQueryPage/LiveQueryPage.tsx | 31 +------------------ .../pages/queries/live/screens/RunQuery.tsx | 16 ++++++++++ 8 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 changes/14874-clip-live-query-results diff --git a/changes/14874-clip-live-query-results b/changes/14874-clip-live-query-results new file mode 100644 index 0000000000..d117992e47 --- /dev/null +++ b/changes/14874-clip-live-query-results @@ -0,0 +1 @@ +* Protect live query performance by limiting number of results per live query diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx index f8cb285466..ccd0d8e783 100644 --- a/frontend/components/LiveQuery/SelectTargets.tsx +++ b/frontend/components/LiveQuery/SelectTargets.tsx @@ -395,8 +395,8 @@ const SelectTargets = ({ return ( <> - {total} host{total > 1 ? `s` : ``} targeted  ( - {onlinePercentage()} + {total.toLocaleString()} host{total > 1 ? `s` : ``}{" "} + targeted  ({onlinePercentage()} % 
- {targetsTotalCount} + {targetsTotalCount.toLocaleString()}  {pluralizeHost(targetsTotalCount)} targeted
diff --git a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx index 1c06cdab66..d871253183 100644 --- a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx +++ b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx @@ -20,12 +20,15 @@ import TabsWrapper from "components/TabsWrapper"; import ShowQueryModal from "components/modals/ShowQueryModal"; import QueryResultsHeading from "components/queries/queryResults/QueryResultsHeading"; import AwaitingResults from "components/queries/queryResults/AwaitingResults"; +import InfoBanner from "components/InfoBanner"; +import CustomLink from "components/CustomLink"; import generateColumnConfigsFromRows from "./QueryResultsTableConfig"; interface IQueryResultsProps { campaign: ICampaign; isQueryFinished: boolean; + isQueryClipped: boolean; queryName?: string; onRunQuery: () => void; onStopQuery: (evt: React.MouseEvent) => void; @@ -44,6 +47,7 @@ const NAV_TITLES = { const QueryResults = ({ campaign, isQueryFinished, + isQueryClipped, queryName, onRunQuery, onStopQuery, @@ -249,6 +253,24 @@ const QueryResults = ({ onClickRunAgain={onRunAgain} onClickStop={onStopQuery} /> + {isQueryClipped && ( + + } + > +
+ Results clipped. A sample of this query's results and + errors is included below. Please target fewer hosts at once to build + a full set of results. +
+
+ )} setNavTabIndex(i)}> @@ -256,7 +278,9 @@ const QueryResults = ({ {errors?.length > 0 && ( - {errors.length} + + {errors.length.toLocaleString()} + )} {NAV_TITLES.ERRORS} diff --git a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx index 5c428e4761..a11ae5a377 100644 --- a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx +++ b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx @@ -9,7 +9,6 @@ import { QueryContext } from "context/query"; import { LIVE_QUERY_STEPS, DOCUMENT_TITLE_SUFFIX } from "utilities/constants"; import queryAPI from "services/entities/queries"; import hostAPI from "services/entities/hosts"; -import statusAPI from "services/entities/status"; import { IHost, IHostResponse } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { ITeam } from "interfaces/team"; @@ -22,7 +21,6 @@ import MainContent from "components/MainContent"; import SelectTargets from "components/LiveQuery/SelectTargets"; import RunQuery from "pages/queries/live/screens/RunQuery"; -import useTeamIdParam from "hooks/useTeamIdParam"; interface IRunQueryPageProps { router: InjectedRouter; @@ -42,25 +40,9 @@ const RunQueryPage = ({ location, }: IRunQueryPageProps): JSX.Element => { const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; - const { - currentTeamName: teamNameForQuery, - teamIdForApi: apiTeamIdForQuery, - } = useTeamIdParam({ - location, - router, - includeAllTeams: true, - includeNoTeam: false, - }); const handlePageError = useErrorHandler(); - const { - isGlobalAdmin, - isGlobalMaintainer, - isAnyTeamMaintainerOrTeamAdmin, - isObserverPlus, - isAnyTeamObserverPlus, - config, - } = useContext(AppContext); + const { config } = useContext(AppContext); const { selectedQueryTargets, setSelectedQueryTargets, @@ -89,7 +71,6 @@ const RunQueryPage = ({ selectedQueryTargetsByType.teams ); const [targetsTotalCount, setTargetsTotalCount] = useState(0); - const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); const disabledLiveQuery = config?.server_settings.live_query_disabled; @@ -147,16 +128,6 @@ const RunQueryPage = ({ } ); - const detectIsFleetQueryRunnable = () => { - statusAPI.live_query().catch(() => { - setIsLiveQueryRunnable(false); - }); - }; - - useEffect(() => { - detectIsFleetQueryRunnable(); - }, [queryId]); - useEffect(() => { setSelectedQueryTargetsByType({ hosts: targetedHosts, diff --git a/frontend/pages/queries/live/screens/RunQuery.tsx b/frontend/pages/queries/live/screens/RunQuery.tsx index e51433435e..a2c5ace15f 100644 --- a/frontend/pages/queries/live/screens/RunQuery.tsx +++ b/frontend/pages/queries/live/screens/RunQuery.tsx @@ -18,6 +18,9 @@ import { ITarget } from "interfaces/target"; import QueryResults from "../../edit/components/QueryResults"; +const RESPONSE_COUNT_ZERO = { results: 0, errors: 0 } as const; +const CAMPAIGN_LIMIT = 250000; + interface IRunQueryProps { storedQuery: IQuery | undefined; selectedTargets: ITarget[]; @@ -39,6 +42,7 @@ const RunQuery = ({ const { renderFlash } = useContext(NotificationContext); const [isQueryFinished, setIsQueryFinished] = useState(false); + const [isQueryClipped, setIsQueryClipped] = useState(false); const [campaignState, setCampaignState] = useState( DEFAULT_CAMPAIGN_STATE ); @@ -47,12 +51,14 @@ const RunQuery = ({ const runQueryInterval = useRef(null); const globalSocket = useRef(null); const previousSocketData = useRef(null); + const responseCount = useRef({ ...RESPONSE_COUNT_ZERO }); const removeSocket = () => { if (globalSocket.current) { globalSocket.current.close(); globalSocket.current = null; previousSocketData.current = null; + responseCount.current = RESPONSE_COUNT_ZERO; } }; @@ -128,6 +134,8 @@ const RunQuery = ({ ...campaignHelpers.updateCampaignState(socketData)(prevCampaignState), }; }); + responseCount.current.results += socketData?.data?.rows?.length ?? 0; + responseCount.current.errors += socketData?.data?.error ? 1 : 0; if ( socketData.type === "status" && @@ -135,6 +143,13 @@ const RunQuery = ({ ) { return teardownDistributedQuery(); } + if ( + responseCount.current.results + responseCount.current.errors >= + CAMPAIGN_LIMIT + ) { + teardownDistributedQuery(); + setIsQueryClipped(true); + } }; }; @@ -204,6 +219,7 @@ const RunQuery = ({ onRunQuery={onRunQuery} onStopQuery={onStopQuery} isQueryFinished={isQueryFinished} + isQueryClipped={isQueryClipped} setSelectedTargets={setSelectedTargets} goToQueryEditor={goToQueryEditor} queryName={storedQuery?.name}