UI - clip live query results (#15760)

## Addresses #14874 
- once 250,000 (results + errors) return, end the query campaign and
display the "clipped" banner
- Format host counts with commas
- misc. cleanup

<img width="1796" alt="Screenshot 2023-12-19 at 4 37 28 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/353ae5a0-24f0-49c5-b48c-555ff83567e9">

<img width="1796" alt="Screenshot 2023-12-19 at 4 37 02 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/e5722dc5-07b4-4173-8eb3-8d00b3f327ab">


## 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 <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling
2023-12-21 09:23:07 -08:00
committed by GitHub
co-authored by Jacob Shandling
parent 2e8da551d0
commit d49255dfdb
8 changed files with 47 additions and 37 deletions
+1
View File
@@ -0,0 +1 @@
* Protect live query performance by limiting number of results per live query
@@ -395,8 +395,8 @@ const SelectTargets = ({
return (
<>
<b>{total}</b>&nbsp;host{total > 1 ? `s` : ``} targeted&nbsp; (
{onlinePercentage()}
<b>{total.toLocaleString()}</b>&nbsp;host{total > 1 ? `s` : ``}{" "}
targeted&nbsp; ({onlinePercentage()}
%&nbsp;
<TooltipWrapper
tipContent={
@@ -40,8 +40,6 @@ const TargetsInput = ({
const selectedTableHeaders = generateTableHeaders(handleRowRemove);
const dropdownHosts =
searchResults && pullAllBy(searchResults, targetedHosts, "display_name");
// const finalSelectedHostTargets =
// targetedHosts && filter(targetedHosts, "display_name");
const isActiveSearch =
!isEmpty(searchText) && (!hasFetchError || isTargetsLoading);
const isSearchError = !isEmpty(searchText) && hasFetchError;
@@ -24,7 +24,7 @@ const generateResultsCountText = (
return `${resultsCount} ${name.slice(0, -1)}`;
}
return `${resultsCount} ${name}`;
return `${resultsCount.toLocaleString()} ${name}`;
};
export default { generateResultsCountText };
@@ -90,7 +90,7 @@ const QuertResultsHeading = ({
<div className={`${baseClass}__query-information`}>
<div className={`${baseClass}__targeted-wrapper`}>
<span className={`${baseClass}__targeted-count`}>
{targetsTotalCount}
{targetsTotalCount.toLocaleString()}
</span>
<span>&nbsp;{pluralizeHost(targetsTotalCount)} targeted</span>
</div>
@@ -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<HTMLButtonElement>) => 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 && (
<InfoBanner
color="yellow"
cta={
<CustomLink
url="https://www.fleetdm.com/support"
text="Get help"
newTab
/>
}
>
<div>
<b>Results clipped.</b> A sample of this query&apos;s results and
errors is included below. Please target fewer hosts at once to build
a full set of results.
</div>
</InfoBanner>
)}
<TabsWrapper>
<Tabs selectedIndex={navTabIndex} onSelect={(i) => setNavTabIndex(i)}>
<TabList>
@@ -256,7 +278,9 @@ const QueryResults = ({
<Tab disabled={!errors?.length}>
<span>
{errors?.length > 0 && (
<span className="count">{errors.length}</span>
<span className="count">
{errors.length.toLocaleString()}
</span>
)}
{NAV_TITLES.ERRORS}
</span>
@@ -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,
@@ -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<ICampaignState>(
DEFAULT_CAMPAIGN_STATE
);
@@ -47,12 +51,14 @@ const RunQuery = ({
const runQueryInterval = useRef<any>(null);
const globalSocket = useRef<any>(null);
const previousSocketData = useRef<any>(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}