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:
co-authored by
Jacob Shandling
parent
2e8da551d0
commit
d49255dfdb
@@ -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> host{total > 1 ? `s` : ``} targeted (
|
||||
{onlinePercentage()}
|
||||
<b>{total.toLocaleString()}</b> host{total > 1 ? `s` : ``}{" "}
|
||||
targeted ({onlinePercentage()}
|
||||
%
|
||||
<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 };
|
||||
|
||||
+1
-1
@@ -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> {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'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}
|
||||
|
||||
Reference in New Issue
Block a user