Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.queryBox {
flex: 1;
min-width: 100px;
position: relative;
width: 100%;
}

.progressBarMask {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.datasetContainer {
display: flex;
max-width: 250px;
}

.selectContainer {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.maxResultsContainer {
display: flex;
}

.maxResultsContainer :global(.ant-select) {
min-width: 80px;
max-width: 80px;
}

.maxResultsContainer :global(.ant-select-selector) {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {Select} from "antd";

import InputLabel from "../../../../components/InputLabel";
import useSearchStore from "../../SearchState";
import {
MAX_RESULTS_OPTIONS,
SEARCH_UI_STATE,
} from "../../SearchState/typings";
import styles from "./index.module.css";


/**
* Renders a dropdown to select the maximum number of search results.
*
* @return
*/
const MaxResultsSelect = () => {
const maxNumResults = useSearchStore((state) => state.maxNumResults);
const updateMaxNumResults = useSearchStore((state) => state.updateMaxNumResults);
const searchUiState = useSearchStore((state) => state.searchUiState);

const isDisabled = searchUiState === SEARCH_UI_STATE.QUERY_ID_PENDING ||
searchUiState === SEARCH_UI_STATE.QUERYING;

const handleChange = (value: number) => {
updateMaxNumResults(value);
};

return (
<div className={styles["maxResultsContainer"]}>
<InputLabel>Limit</InputLabel>
<Select
disabled={isDisabled}
popupMatchSelectWidth={false}
size={"middle"}
value={maxNumResults}
options={MAX_RESULTS_OPTIONS.map((option) => ({
label: option.toLocaleString(),
value: option,
}))}
onChange={handleChange}/>
</div>
);
};

export default MaxResultsSelect;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {CLP_STORAGE_ENGINES} from "@webui/common/config";
import {SETTINGS_STORAGE_ENGINE} from "../../../../config";
import Dataset from "../Dataset";
import styles from "../index.module.css";
import MaxResultsSelect from "../MaxResultsSelect";
import QueryStatus from "../QueryStatus";
import TimeRangeInput from "../TimeRangeInput";
import QueryInput from "./QueryInput";
Expand All @@ -20,6 +21,7 @@ const NativeControls = () => (
{CLP_STORAGE_ENGINES.CLP_S === SETTINGS_STORAGE_ENGINE && <Dataset/>}
<QueryInput/>
<TimeRangeInput/>
<MaxResultsSelect/>
<SearchButton/>
</div>
<div className={styles["status"]}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const SubmitButton = () => {
);
const queryString = useSearchStore((state) => state.queryString);
const selectedDatasets = useSearchStore((state) => state.selectedDatasets);
const maxNumResults = useSearchStore((state) => state.maxNumResults);
const updateQueriedDatasets = useSearchStore((state) => state.updateQueriedDatasets);
const [messageApi, contextHolder] = message.useMessage();

Expand Down Expand Up @@ -79,6 +80,7 @@ const SubmitButton = () => {
handleQuerySubmit({
datasets: selectedDatasets,
ignoreCase: false === queryIsCaseSensitive,
maxNumResults: maxNumResults,
queryString: queryString,
timeRangeBucketSizeMillis: newTimelineConfig.bucketDuration.asMilliseconds(),
...(
Expand All @@ -102,6 +104,7 @@ const SubmitButton = () => {
messageApi,
selectedDatasets,
updateQueriedDatasets,
maxNumResults,
]);

const isQueryStringEmpty = "" === queryString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

.rangePicker {
min-width: 350px;
max-width: 350px;
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

.inputsAndButtonRow {
display: flex;
flex-wrap: wrap;
gap: 10px;
}

Expand All @@ -15,6 +16,7 @@

.runRow {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import MongoSocketCollection from "../../../../../../api/socket/MongoSocketCollection";
import {useCursor} from "../../../../../../api/socket/useCursor";
import useSearchStore, {SEARCH_STATE_DEFAULT} from "../../../../SearchState/index";
import {SEARCH_MAX_NUM_RESULTS} from "../../typings";
import {SearchResult} from "./typings";


Expand All @@ -11,7 +10,7 @@ import {SearchResult} from "./typings";
* @return
*/
const useSearchResults = () => {
const {searchJobId} = useSearchStore();
const {searchJobId, maxNumResults} = useSearchStore();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify full-state Zustand subscriptions in SearchPage code.
rg -nP --type=ts --type=tsx 'useSearchStore\s*\(\s*\)' components/webui/client/src/pages/SearchPage

Repository: y-scope/clp

Length of output: 83


🏁 Script executed:

# First, let's read the target file to verify the code at line 13
cat -n components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/useSearchResults.ts | head -30

Repository: y-scope/clp

Length of output: 1276


🏁 Script executed:

# Search for full-state Zustand subscriptions using correct file type flags
rg -nP --type=ts 'useSearchStore\s*\(\s*\)' components/webui/client/src/pages/SearchPage

Repository: y-scope/clp

Length of output: 1376


🏁 Script executed:

# Also search for useSearchStore definition to understand the store structure
fd -e ts -e tsx useSearchStore | head -5

Repository: y-scope/clp

Length of output: 37


🏁 Script executed:

# Find the SearchState index file to see useSearchStore definition
cat -n components/webui/client/src/pages/SearchPage/SearchState/index.ts | head -100

Repository: y-scope/clp

Length of output: 153


🏁 Script executed:

# Check if there are any examples of selective Zustand subscriptions in the codebase
rg -A2 'useSearchStore\s*\(\s*\(\s*state\s*\)' components/webui/client/src/pages/SearchPage

Repository: y-scope/clp

Length of output: 19282


🏁 Script executed:

# Also search broader for state selector patterns
rg '\(\s*state\s*\)\s*=>' components/webui/client/src/pages/SearchPage --type=ts --type=tsx -A1 | head -30

Repository: y-scope/clp

Length of output: 83


🏁 Script executed:

# Find the SearchState definition file with different extensions
fd -e js -e jsx -e ts -e tsx SearchState/index components/webui/client/src/pages/SearchPage

Repository: y-scope/clp

Length of output: 37


🏁 Script executed:

# Also search for the file in the parent SearchState directory
ls -la components/webui/client/src/pages/SearchPage/SearchState/

Repository: y-scope/clp

Length of output: 562


🏁 Script executed:

# Read the SearchState store definition to confirm Zustand usage
cat -n components/webui/client/src/pages/SearchPage/SearchState/index.tsx | head -80

Repository: y-scope/clp

Length of output: 2874


Use selective Zustand store subscriptions instead of full-state destructuring.

This hook subscribes to all store property changes when only searchJobId and maxNumResults are needed. Using selector functions limits re-renders to changes in those specific properties, improving performance and following the established pattern throughout the codebase.

Proposed refactor
-    const {searchJobId, maxNumResults} = useSearchStore();
+    const searchJobId = useSearchStore((state) => state.searchJobId);
+    const maxNumResults = useSearchStore((state) => state.maxNumResults);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {searchJobId, maxNumResults} = useSearchStore();
const searchJobId = useSearchStore((state) => state.searchJobId);
const maxNumResults = useSearchStore((state) => state.maxNumResults);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@components/webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/useSearchResults.ts`
at line 13, The hook in useSearchResults.ts currently destructures the entire
useSearchStore state with "const {searchJobId, maxNumResults} =
useSearchStore()", causing subscriptions to all state changes; change this to
selective Zustand selectors by calling useSearchStore with a selector function
that returns only searchJobId and maxNumResults (e.g., useSearchStore(s => ({
searchJobId: s.searchJobId, maxNumResults: s.maxNumResults }))) or two separate
calls to subscribe individually, so the component only re-renders when those
specific properties change; update references in the file to use the new
selector-returned values.


const searchResultsCursor = useCursor<SearchResult>(
() => {
Expand All @@ -25,7 +24,6 @@ const useSearchResults = () => {
`Subscribing to updates to search results with job ID: ${searchJobId}`
);

// Retrieve 1k most recent results.
const options = {
sort: [
[
Expand All @@ -37,13 +35,16 @@ const useSearchResults = () => {
"desc",
],
],
limit: SEARCH_MAX_NUM_RESULTS,
limit: maxNumResults,
};

const collection = new MongoSocketCollection(searchJobId);
return collection.find({}, options);
},
[searchJobId]
[
searchJobId,
maxNumResults,
]
);

return searchResultsCursor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const NativeResultsTimeline = () => {
const searchUiState = useSearchStore((state) => state.searchUiState);
const selectedDatasets = useSearchStore((state) => state.selectedDatasets);
const timelineConfig = useSearchStore((state) => state.timelineConfig);
const maxNumResults = useSearchStore((state) => state.maxNumResults);

const aggregationResults = useAggregationResults();

Expand Down Expand Up @@ -63,12 +64,14 @@ const NativeResultsTimeline = () => {
handleQuerySubmit({
datasets: selectedDatasets,
ignoreCase: false === queryIsCaseSensitive,
maxNumResults: maxNumResults,
queryString: queryString,
timeRangeBucketSizeMillis: newTimelineConfig.bucketDuration.asMilliseconds(),
timestampBegin: newTimeRange[0].valueOf(),
timestampEnd: newTimeRange[1].valueOf(),
});
}, [
maxNumResults,
queryIsCaseSensitive,
queryString,
selectedDatasets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ import {
} from "../SearchResults/SearchResultsTable/Native/SearchResultsVirtualTable/typings";
import {formatExportFilenameTimestamp} from "../SearchResults/SearchResultsTable/Native/utils";
import {computeTimelineConfig} from "../SearchResults/SearchResultsTimeline/utils";
import {SEARCH_UI_STATE} from "./typings";
import {
DEFAULT_MAX_NUM_RESULTS,
SEARCH_UI_STATE,
} from "./typings";


/**
* Default values of the search state.
*/
const SEARCH_STATE_DEFAULT = Object.freeze({
aggregationJobId: null,
maxNumResults: DEFAULT_MAX_NUM_RESULTS,
numSearchResultsMetadata: 0,
numSearchResultsTable: 0,
numSearchResultsTimeline: 0,
Expand All @@ -44,6 +48,11 @@ interface SearchState {
*/
aggregationJobId: string | null;

/**
* Maximum number of search results to retrieve.
*/
maxNumResults: number;

/**
* The number of search results from server metadata.
*/
Expand Down Expand Up @@ -121,6 +130,7 @@ interface SearchState {
handleSearchResultsExport: () => void;

updateAggregationJobId: (id: string | null) => void;
updateMaxNumResults: (max: number) => void;
updateNumSearchResultsMetadata: (num: number) => void;
updateNumSearchResultsTable: (num: number) => void;
updateNumSearchResultsTimeline: (num: number) => void;
Expand Down Expand Up @@ -159,6 +169,9 @@ const useSearchStore = create<SearchState>((set, get) => ({
updateAggregationJobId: (id) => {
set({aggregationJobId: id});
},
updateMaxNumResults: (max) => {
set({maxNumResults: max});
},
updateNumSearchResultsMetadata: (num) => {
set({numSearchResultsMetadata: num});
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
/**
* Available options for the maximum number of search results.
*/
/* eslint-disable no-magic-numbers */
const MAX_RESULTS_OPTIONS = [
10,
50,
100,
500,
1000,
5000,
10000,
] as const;
/* eslint-enable no-magic-numbers */

/**
* Default value for the maximum number of search results.
*/
const DEFAULT_MAX_NUM_RESULTS: number = 1000;

/**
* Search UI states.
*/
Expand Down Expand Up @@ -28,4 +48,8 @@ enum SEARCH_UI_STATE {
FAILED,
}

export {SEARCH_UI_STATE};
export {
DEFAULT_MAX_NUM_RESULTS,
MAX_RESULTS_OPTIONS,
SEARCH_UI_STATE,
};
1 change: 1 addition & 0 deletions components/webui/common/src/schemas/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
const QueryJobCreationSchema = Type.Object({
datasets: Type.Array(Type.String()),
ignoreCase: Type.Boolean(),
maxNumResults: Type.Optional(Type.Integer({minimum: 1})),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an upper bound for maxNumResults in the request schema.

minimum: 1 alone lets callers send unbounded limits (for example via direct API calls), which can drive expensive query and fetch paths. Please enforce the same ceiling used by product behaviour (10,000) at schema level.

Suggested patch
-    maxNumResults: Type.Optional(Type.Integer({minimum: 1})),
+    maxNumResults: Type.Optional(Type.Integer({
+        minimum: 1,
+        maximum: 10000,
+    })),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
maxNumResults: Type.Optional(Type.Integer({minimum: 1})),
maxNumResults: Type.Optional(Type.Integer({
minimum: 1,
maximum: 10000,
})),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/webui/common/src/schemas/search.ts` at line 18, The schema for
maxNumResults currently only specifies Type.Optional(Type.Integer({minimum:
1})); update the field to enforce an upper bound of 10000 so callers cannot
request unbounded results — e.g. change the integer constraints to include
maximum: 10000 while keeping the field optional (modify the maxNumResults
declaration in components/webui/common/src/schemas/search.ts to use
Type.Integer({minimum: 1, maximum: 10000})).

queryString: StringSchema,
timeRangeBucketSizeMillis: Type.Integer(),
timestampBegin: Type.Union([Type.Null(),
Expand Down
6 changes: 4 additions & 2 deletions components/webui/server/src/routes/api/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import {constants} from "http2";

import settings from "../../../../settings.json" with {type: "json"};
import {SEARCH_MAX_NUM_RESULTS} from "./typings.js";
import {DEFAULT_SEARCH_MAX_NUM_RESULTS} from "./typings.js";
import {
createMongoIndexes,
updateSearchSignalWhenJobsFinish,
Expand Down Expand Up @@ -68,6 +68,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
timestampBegin,
timestampEnd,
ignoreCase,
maxNumResults,
timeRangeBucketSizeMillis,
queryString,
} = request.body;
Expand All @@ -79,7 +80,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
null,
end_timestamp: timestampEnd,
ignore_case: ignoreCase,
max_num_results: SEARCH_MAX_NUM_RESULTS,
max_num_results: maxNumResults ?? DEFAULT_SEARCH_MAX_NUM_RESULTS,
query_string: queryString,
};

Expand Down Expand Up @@ -126,6 +127,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
updateSearchSignalWhenJobsFinish({
aggregationJobId: aggregationJobId,
logger: request.log,
maxNumResults: maxNumResults ?? DEFAULT_SEARCH_MAX_NUM_RESULTS,
mongoDb: mongoDb,
queryJobDbManager: QueryJobDbManager,
searchJobId: searchJobId,
Expand Down
6 changes: 3 additions & 3 deletions components/webui/server/src/routes/api/search/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import type {
/**
* The maximum number of results to retrieve for a search.
*/
const SEARCH_MAX_NUM_RESULTS = 1000;
const DEFAULT_SEARCH_MAX_NUM_RESULTS = 1000;

type UpdateSearchSignalWhenJobsFinishProps = {
aggregationJobId: number;
logger: FastifyBaseLogger;
maxNumResults: number;
mongoDb: Db;
queryJobDbManager: FastifyInstance["QueryJobDbManager"];
searchJobId: number;
Expand All @@ -32,7 +33,6 @@ type CreateMongoIndexesProps = {

export {
CreateMongoIndexesProps,
SEARCH_MAX_NUM_RESULTS,
SearchResultsMetadataDocument,
DEFAULT_SEARCH_MAX_NUM_RESULTS,
UpdateSearchSignalWhenJobsFinishProps,
};
7 changes: 4 additions & 3 deletions components/webui/server/src/routes/api/search/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {Db} from "mongodb";

import {
CreateMongoIndexesProps,
SEARCH_MAX_NUM_RESULTS,
UpdateSearchSignalWhenJobsFinishProps,
} from "./typings.js";

Expand All @@ -26,14 +25,16 @@ const hasCollection = async (mongoDb: Db, collectionName: string): Promise<boole
* @param props
* @param props.aggregationJobId
* @param props.logger
* @param props.maxNumResults
* @param props.mongoDb
* @param props.queryJobDbManager
* @param props.searchJobId
* @param props.searchResultsMetadataCollection
* @param props.mongoDb
*/
const updateSearchSignalWhenJobsFinish = async ({
aggregationJobId,
logger,
maxNumResults,
mongoDb,
queryJobDbManager,
searchJobId,
Expand Down Expand Up @@ -82,7 +83,7 @@ const updateSearchSignalWhenJobsFinish = async ({
errorMsg: errorMsg,
numTotalResults: Math.min(
numResultsInCollection,
SEARCH_MAX_NUM_RESULTS
maxNumResults
),
},
};
Expand Down
Loading