From 061b4cb0c6d19148f6bf68f301c466019de23d26 Mon Sep 17 00:00:00 2001 From: openshift-trt Date: Tue, 30 Jun 2026 16:43:03 +0000 Subject: [PATCH 1/7] TRT-2764: Fix search bar not completing search until page refresh The requestSearch function in 11 table components mutated the filterModel object returned by useStableJSONQueryParam in place. This corrupted the hook's internal ref state: the intermediate re-render from setSearching(true) would detect the mutation via the defaultValue fallback, update the serialized cache, and then when the URL update arrived with the same content, the comparison found no change, so the useEffect never fired and fetchData was never called. Fix by creating a new object with spread syntax instead of mutating the existing filterModel. This ensures useStableJSONQueryParam sees a genuinely new reference on re-render, triggering the useEffect and completing the search. Co-Authored-By: Claude Opus 4.6 --- sippy-ng/src/jobs/JobRunsTable.js | 12 ++++++------ sippy-ng/src/jobs/JobTable.js | 12 ++++++------ sippy-ng/src/releases/PayloadStreamTestFailures.js | 12 ++++++------ sippy-ng/src/releases/PayloadStreamsTable.js | 10 ++++++---- sippy-ng/src/releases/PayloadTestFailures.js | 12 ++++++------ sippy-ng/src/releases/ReleasePayloadJobRuns.js | 10 ++++++---- sippy-ng/src/releases/ReleasePayloadPullRequests.js | 10 ++++++---- sippy-ng/src/releases/ReleasePayloadTable.js | 10 ++++++---- sippy-ng/src/repositories/RepositoriesTable.js | 12 ++++++------ sippy-ng/src/tests/FeatureGates.js | 10 ++++++---- sippy-ng/src/tests/TestTable.js | 12 ++++++------ 11 files changed, 66 insertions(+), 56 deletions(-) diff --git a/sippy-ng/src/jobs/JobRunsTable.js b/sippy-ng/src/jobs/JobRunsTable.js index 81c9e4ab71..605bf1c81b 100644 --- a/sippy-ng/src/jobs/JobRunsTable.js +++ b/sippy-ng/src/jobs/JobRunsTable.js @@ -468,17 +468,17 @@ export default function JobRunsTable(props) { } const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'job' - ) - currentFilters.items.push({ + const newItems = filterModel.items.filter((f) => f.columnField !== 'job') + newItems.push({ id: 99, columnField: 'job', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } useEffect(() => { diff --git a/sippy-ng/src/jobs/JobTable.js b/sippy-ng/src/jobs/JobTable.js index 1d8eea3cb8..337218a8b1 100644 --- a/sippy-ng/src/jobs/JobTable.js +++ b/sippy-ng/src/jobs/JobTable.js @@ -533,17 +533,17 @@ function JobTable(props) { } const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'name' - ) - currentFilters.items.push({ + const newItems = filterModel.items.filter((f) => f.columnField !== 'name') + newItems.push({ id: 99, columnField: 'name', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } useEffect(() => { diff --git a/sippy-ng/src/releases/PayloadStreamTestFailures.js b/sippy-ng/src/releases/PayloadStreamTestFailures.js index 878b959e65..f192b15666 100644 --- a/sippy-ng/src/releases/PayloadStreamTestFailures.js +++ b/sippy-ng/src/releases/PayloadStreamTestFailures.js @@ -116,17 +116,17 @@ function PayloadStreamTestFailures(props) { ) const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'name' - ) - currentFilters.items.push({ + const newItems = filterModel.items.filter((f) => f.columnField !== 'name') + newItems.push({ id: 99, columnField: 'name', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/releases/PayloadStreamsTable.js b/sippy-ng/src/releases/PayloadStreamsTable.js index 5095857009..d3470faab6 100644 --- a/sippy-ng/src/releases/PayloadStreamsTable.js +++ b/sippy-ng/src/releases/PayloadStreamsTable.js @@ -127,17 +127,19 @@ function PayloadStreamsTable(props) { ) const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( + const newItems = filterModel.items.filter( (f) => f.columnField !== 'release_tag' ) - currentFilters.items.push({ + newItems.push({ id: 99, columnField: 'release_tag', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/releases/PayloadTestFailures.js b/sippy-ng/src/releases/PayloadTestFailures.js index 34b264d491..5b7ef0ac01 100644 --- a/sippy-ng/src/releases/PayloadTestFailures.js +++ b/sippy-ng/src/releases/PayloadTestFailures.js @@ -95,17 +95,17 @@ function PayloadTestFailures(props) { ) const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'name' - ) - currentFilters.items.push({ + const newItems = filterModel.items.filter((f) => f.columnField !== 'name') + newItems.push({ id: 99, columnField: 'name', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/releases/ReleasePayloadJobRuns.js b/sippy-ng/src/releases/ReleasePayloadJobRuns.js index 86123b7d52..1aaf8fb287 100644 --- a/sippy-ng/src/releases/ReleasePayloadJobRuns.js +++ b/sippy-ng/src/releases/ReleasePayloadJobRuns.js @@ -155,17 +155,19 @@ function ReleasePayloadJobRuns(props) { ) const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( + const newItems = filterModel.items.filter( (f) => f.columnField !== 'release_tag' ) - currentFilters.items.push({ + newItems.push({ id: 99, columnField: 'releaseTag', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/releases/ReleasePayloadPullRequests.js b/sippy-ng/src/releases/ReleasePayloadPullRequests.js index 60794905b2..220799434d 100644 --- a/sippy-ng/src/releases/ReleasePayloadPullRequests.js +++ b/sippy-ng/src/releases/ReleasePayloadPullRequests.js @@ -77,17 +77,19 @@ function ReleasePayloadPullRequests(props) { ) const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( + const newItems = filterModel.items.filter( (f) => f.columnField !== 'release_tag' ) - currentFilters.items.push({ + newItems.push({ id: 99, columnField: 'release_tag', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/releases/ReleasePayloadTable.js b/sippy-ng/src/releases/ReleasePayloadTable.js index a9412f6ad6..46eb540c3c 100644 --- a/sippy-ng/src/releases/ReleasePayloadTable.js +++ b/sippy-ng/src/releases/ReleasePayloadTable.js @@ -320,17 +320,19 @@ function ReleasePayloadTable(props) { } const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( + const newItems = filterModel.items.filter( (f) => f.columnField !== 'release_tag' ) - currentFilters.items.push({ + newItems.push({ id: 99, columnField: 'release_tag', operatorValue: 'contains', value: searchValue, }) - setFilterModelWithConversion(currentFilters) + setFilterModelWithConversion({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/repositories/RepositoriesTable.js b/sippy-ng/src/repositories/RepositoriesTable.js index c25ba537b8..9d57eceaf7 100644 --- a/sippy-ng/src/repositories/RepositoriesTable.js +++ b/sippy-ng/src/repositories/RepositoriesTable.js @@ -278,17 +278,17 @@ function RepositoriesTable(props) { } const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'repo' - ) - currentFilters.items.push({ + const newItems = filterModel.items.filter((f) => f.columnField !== 'repo') + newItems.push({ id: 99, columnField: 'repo', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } return ( diff --git a/sippy-ng/src/tests/FeatureGates.js b/sippy-ng/src/tests/FeatureGates.js index 31ac07999c..cce14f9c84 100644 --- a/sippy-ng/src/tests/FeatureGates.js +++ b/sippy-ng/src/tests/FeatureGates.js @@ -211,17 +211,19 @@ export default function FeatureGates(props) { } const requestSearch = (searchValue) => { - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( + const newItems = filterModel.items.filter( (f) => f.columnField !== 'feature_gate' ) - currentFilters.items.push({ + newItems.push({ id: 99, columnField: 'feature_gate', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } const addFilters = (filter) => { diff --git a/sippy-ng/src/tests/TestTable.js b/sippy-ng/src/tests/TestTable.js index 68fd3701ce..734ef93492 100644 --- a/sippy-ng/src/tests/TestTable.js +++ b/sippy-ng/src/tests/TestTable.js @@ -1004,17 +1004,17 @@ function TestTable(props) { const requestSearch = (searchValue) => { setSearching(true) - const currentFilters = filterModel - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'name' - ) - currentFilters.items.push({ + const newItems = filterModel.items.filter((f) => f.columnField !== 'name') + newItems.push({ id: 99, columnField: 'name', operatorValue: 'contains', value: searchValue, }) - setFilterModel(currentFilters) + setFilterModel({ + ...filterModel, + items: newItems, + }) } if (fetchError !== '') { From 45601e8e60044e39c4d4c26d8a77b6d8a7e640da Mon Sep 17 00:00:00 2001 From: openshift-trt Date: Wed, 1 Jul 2026 14:07:31 +0000 Subject: [PATCH 2/7] TRT-2764: Fix double-search stuck loading and repopulate search bar on refresh Fix two issues with the quick search bar: 1. Pressing Enter or clicking the search icon when the same search term is already active no longer gets stuck in loading state. The requestSearch function in TestTable now checks if the filter would actually change before setting the searching state. 2. When refreshing the page, the quick search bar is now repopulated from the filterModel URL query parameter. GridToolbar accepts a new searchField prop that identifies which filter column corresponds to the search bar, and initializes the search text on mount. Co-Authored-By: Claude Opus 4.6 --- .../RegressedTestsPanel.js | 1 + .../TriagedRegressionTestList.js | 1 + .../component_readiness/TriagedRegressions.js | 1 + sippy-ng/src/datagrid/GridToolbar.js | 23 ++++++++++++++++++- sippy-ng/src/jobs/JobAnalysis.js | 1 + sippy-ng/src/jobs/JobRunsTable.js | 1 + sippy-ng/src/jobs/JobTable.js | 1 + .../src/releases/PayloadStreamTestFailures.js | 1 + sippy-ng/src/releases/PayloadStreamsTable.js | 1 + sippy-ng/src/releases/PayloadTestFailures.js | 1 + .../src/releases/ReleasePayloadJobRuns.js | 1 + .../releases/ReleasePayloadPullRequests.js | 1 + sippy-ng/src/releases/ReleasePayloadTable.js | 1 + .../src/repositories/RepositoriesTable.js | 1 + sippy-ng/src/tests/FeatureGates.js | 1 + sippy-ng/src/tests/TestTable.js | 7 ++++++ 16 files changed, 43 insertions(+), 1 deletion(-) diff --git a/sippy-ng/src/component_readiness/RegressedTestsPanel.js b/sippy-ng/src/component_readiness/RegressedTestsPanel.js index 0252365448..105bc4f744 100644 --- a/sippy-ng/src/component_readiness/RegressedTestsPanel.js +++ b/sippy-ng/src/component_readiness/RegressedTestsPanel.js @@ -373,6 +373,7 @@ export default function RegressedTestsPanel(props) { setFilterModel: setFilterModel, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'test_name', autocompleteData: regressedTests, downloadDataFunc: () => { return filteredTests diff --git a/sippy-ng/src/component_readiness/TriagedRegressionTestList.js b/sippy-ng/src/component_readiness/TriagedRegressionTestList.js index 5b047b74bc..ff763650bc 100644 --- a/sippy-ng/src/component_readiness/TriagedRegressionTestList.js +++ b/sippy-ng/src/component_readiness/TriagedRegressionTestList.js @@ -352,6 +352,7 @@ export default function TriagedRegressionTestList(props) { setFilterModel: setFilterModel, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'test_name', autocompleteData: triagedRegressions, downloadDataFunc: () => { return filteredRegressions diff --git a/sippy-ng/src/component_readiness/TriagedRegressions.js b/sippy-ng/src/component_readiness/TriagedRegressions.js index cc8c28a423..41fbeacc54 100644 --- a/sippy-ng/src/component_readiness/TriagedRegressions.js +++ b/sippy-ng/src/component_readiness/TriagedRegressions.js @@ -407,6 +407,7 @@ export default function TriagedRegressions({ setFilterModel: setFilterModel, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'description', autocompleteData: triageEntries, downloadDataFunc: () => { return filteredTriageEntries diff --git a/sippy-ng/src/datagrid/GridToolbar.js b/sippy-ng/src/datagrid/GridToolbar.js index 602a3f640d..4b7652a875 100644 --- a/sippy-ng/src/datagrid/GridToolbar.js +++ b/sippy-ng/src/datagrid/GridToolbar.js @@ -8,7 +8,7 @@ import GridToolbarPeriodSelector from '../datagrid/GridToolbarPeriodSelector' import GridToolbarViewSelector from './GridToolbarViewSelector' import IconButton from '@mui/material/IconButton' import PropTypes from 'prop-types' -import React, { Fragment } from 'react' +import React, { Fragment, useEffect, useRef } from 'react' import SearchIcon from '@mui/icons-material/Search' import TextField from '@mui/material/TextField' @@ -39,6 +39,26 @@ export default function GridToolbar(props) { const classes = useStyles(theme) const [search, setSearch] = React.useState('') + const initializedFromFilter = useRef(false) + + useEffect(() => { + if ( + initializedFromFilter.current || + !props.searchField || + !props.filterModel || + !props.filterModel.items + ) { + return + } + const searchFilter = props.filterModel.items.find( + (f) => + f.columnField === props.searchField && f.operatorValue === 'contains' + ) + if (searchFilter && searchFilter.value) { + setSearch(searchFilter.value) + } + initializedFromFilter.current = true + }, [props.searchField, props.filterModel]) return (
@@ -145,4 +165,5 @@ GridToolbar.propTypes = { downloadDataFunc: PropTypes.func, downloadFilePrefix: PropTypes.string, autocompleteData: PropTypes.array, + searchField: PropTypes.string, } diff --git a/sippy-ng/src/jobs/JobAnalysis.js b/sippy-ng/src/jobs/JobAnalysis.js index 363faa9f68..42bef2e807 100644 --- a/sippy-ng/src/jobs/JobAnalysis.js +++ b/sippy-ng/src/jobs/JobAnalysis.js @@ -602,6 +602,7 @@ export function JobAnalysis(props) { setFilterModel: setTestFilter, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'name', }, }} /> diff --git a/sippy-ng/src/jobs/JobRunsTable.js b/sippy-ng/src/jobs/JobRunsTable.js index 605bf1c81b..2fc6a3a813 100644 --- a/sippy-ng/src/jobs/JobRunsTable.js +++ b/sippy-ng/src/jobs/JobRunsTable.js @@ -655,6 +655,7 @@ export default function JobRunsTable(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'job', filterModel: filterModel, setFilterModel: setFilterModel, addFilters: (m) => addFilters(m), diff --git a/sippy-ng/src/jobs/JobTable.js b/sippy-ng/src/jobs/JobTable.js index 337218a8b1..972595dea7 100644 --- a/sippy-ng/src/jobs/JobTable.js +++ b/sippy-ng/src/jobs/JobTable.js @@ -708,6 +708,7 @@ function JobTable(props) { columns: gridView.filterColumns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'name', period: period, selectPeriod: setPeriod, addFilters: (m) => addFilters(m), diff --git a/sippy-ng/src/releases/PayloadStreamTestFailures.js b/sippy-ng/src/releases/PayloadStreamTestFailures.js index f192b15666..96ab7f02e3 100644 --- a/sippy-ng/src/releases/PayloadStreamTestFailures.js +++ b/sippy-ng/src/releases/PayloadStreamTestFailures.js @@ -257,6 +257,7 @@ function PayloadStreamTestFailures(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'name', addFilters: addFilters, filterModel: filterModel, setFilterModel: setFilterModel, diff --git a/sippy-ng/src/releases/PayloadStreamsTable.js b/sippy-ng/src/releases/PayloadStreamsTable.js index d3470faab6..a4cb9a581f 100644 --- a/sippy-ng/src/releases/PayloadStreamsTable.js +++ b/sippy-ng/src/releases/PayloadStreamsTable.js @@ -248,6 +248,7 @@ function PayloadStreamsTable(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'release_tag', addFilters: addFilters, filterModel: filterModel, setFilterModel: setFilterModel, diff --git a/sippy-ng/src/releases/PayloadTestFailures.js b/sippy-ng/src/releases/PayloadTestFailures.js index 5b7ef0ac01..4773972e12 100644 --- a/sippy-ng/src/releases/PayloadTestFailures.js +++ b/sippy-ng/src/releases/PayloadTestFailures.js @@ -231,6 +231,7 @@ function PayloadTestFailures(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'name', addFilters: addFilters, filterModel: filterModel, setFilterModel: setFilterModel, diff --git a/sippy-ng/src/releases/ReleasePayloadJobRuns.js b/sippy-ng/src/releases/ReleasePayloadJobRuns.js index 1aaf8fb287..047eb4d4ac 100644 --- a/sippy-ng/src/releases/ReleasePayloadJobRuns.js +++ b/sippy-ng/src/releases/ReleasePayloadJobRuns.js @@ -343,6 +343,7 @@ function ReleasePayloadJobRuns(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'release_tag', addFilters: addFilters, filterModel: filterModel, setFilterModel: setFilterModel, diff --git a/sippy-ng/src/releases/ReleasePayloadPullRequests.js b/sippy-ng/src/releases/ReleasePayloadPullRequests.js index 220799434d..a304adf452 100644 --- a/sippy-ng/src/releases/ReleasePayloadPullRequests.js +++ b/sippy-ng/src/releases/ReleasePayloadPullRequests.js @@ -197,6 +197,7 @@ function ReleasePayloadPullRequests(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'release_tag', addFilters: addFilters, filterModel: filterModel, setFilterModel: setFilterModel, diff --git a/sippy-ng/src/releases/ReleasePayloadTable.js b/sippy-ng/src/releases/ReleasePayloadTable.js index 46eb540c3c..4920d9f6ae 100644 --- a/sippy-ng/src/releases/ReleasePayloadTable.js +++ b/sippy-ng/src/releases/ReleasePayloadTable.js @@ -445,6 +445,7 @@ function ReleasePayloadTable(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'release_tag', addFilters: addFilters, filterModel: filterModel, setFilterModel: setFilterModelWithConversion, diff --git a/sippy-ng/src/repositories/RepositoriesTable.js b/sippy-ng/src/repositories/RepositoriesTable.js index 9d57eceaf7..8851d3ccc3 100644 --- a/sippy-ng/src/repositories/RepositoriesTable.js +++ b/sippy-ng/src/repositories/RepositoriesTable.js @@ -328,6 +328,7 @@ function RepositoriesTable(props) { componentsProps={{ toolbar: { doSearch: requestSearch, + searchField: 'repo', clearSearch: () => requestSearch(''), views: gridView.views, view: view, diff --git a/sippy-ng/src/tests/FeatureGates.js b/sippy-ng/src/tests/FeatureGates.js index cce14f9c84..d268aeb9e3 100644 --- a/sippy-ng/src/tests/FeatureGates.js +++ b/sippy-ng/src/tests/FeatureGates.js @@ -403,6 +403,7 @@ export default function FeatureGates(props) { columns: columns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'feature_gate', addFilters: (m) => addFilters(m), filterModel: filterModel, setFilterModel: setFilterModel, diff --git a/sippy-ng/src/tests/TestTable.js b/sippy-ng/src/tests/TestTable.js index 734ef93492..8e4c4eed46 100644 --- a/sippy-ng/src/tests/TestTable.js +++ b/sippy-ng/src/tests/TestTable.js @@ -1003,6 +1003,12 @@ function TestTable(props) { ]) const requestSearch = (searchValue) => { + const existingFilter = filterModel.items.find( + (f) => f.columnField === 'name' && f.operatorValue === 'contains' + ) + if (existingFilter && existingFilter.value === searchValue) { + return + } setSearching(true) const newItems = filterModel.items.filter((f) => f.columnField !== 'name') newItems.push({ @@ -1123,6 +1129,7 @@ function TestTable(props) { columns: gridView.filterColumns, clearSearch: () => requestSearch(''), doSearch: requestSearch, + searchField: 'name', period: period, selectPeriod: setPeriod, addFilters: addFilters, From 9ca19ddfa0571fedd2dcbfba949ebab5f9277385 Mon Sep 17 00:00:00 2001 From: openshift-trt Date: Wed, 1 Jul 2026 15:39:17 +0000 Subject: [PATCH 3/7] TRT-2764: Fix double-search stuck loading and search bar repopulation Two fixes in GridToolbar: 1. Remove onBlur handler from search TextField to prevent double-trigger when clicking the search icon (onBlur fires before onClick, causing two requestSearch calls that can race with state updates and leave isSearching stuck as true). 2. Only set initializedFromFilter ref when a search filter is actually found, so the search bar gets repopulated from URL params on page refresh even if the filterModel is initially empty on first render. Co-Authored-By: Claude Opus 4.6 --- sippy-ng/src/datagrid/GridToolbar.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sippy-ng/src/datagrid/GridToolbar.js b/sippy-ng/src/datagrid/GridToolbar.js index 4b7652a875..46c18c5fff 100644 --- a/sippy-ng/src/datagrid/GridToolbar.js +++ b/sippy-ng/src/datagrid/GridToolbar.js @@ -56,8 +56,8 @@ export default function GridToolbar(props) { ) if (searchFilter && searchFilter.value) { setSearch(searchFilter.value) + initializedFromFilter.current = true } - initializedFromFilter.current = true }, [props.searchField, props.filterModel]) return ( @@ -112,7 +112,6 @@ export default function GridToolbar(props) { value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && props.doSearch(search)} - onBlur={() => props.doSearch(search)} placeholder="Search…" InputProps={{ endAdornment: ( From db0562a518d3d3ae3260a27428789fef015f87e2 Mon Sep 17 00:00:00 2001 From: openshift-trt Date: Thu, 2 Jul 2026 01:31:15 +0000 Subject: [PATCH 4/7] TRT-2764: Only populate quick search from single positive filter Only initialize the search bar from the URL filterModel when there is exactly one filter on the search field and it uses a positive 'contains' operator. This avoids misleadingly populating the search bar for NOT matchers or when multiple filters exist on the same column. Co-Authored-By: Claude Opus 4.6 --- sippy-ng/src/datagrid/GridToolbar.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sippy-ng/src/datagrid/GridToolbar.js b/sippy-ng/src/datagrid/GridToolbar.js index 46c18c5fff..f96889333f 100644 --- a/sippy-ng/src/datagrid/GridToolbar.js +++ b/sippy-ng/src/datagrid/GridToolbar.js @@ -50,12 +50,15 @@ export default function GridToolbar(props) { ) { return } - const searchFilter = props.filterModel.items.find( - (f) => - f.columnField === props.searchField && f.operatorValue === 'contains' + const filtersForField = props.filterModel.items.filter( + (f) => f.columnField === props.searchField ) - if (searchFilter && searchFilter.value) { - setSearch(searchFilter.value) + if (filtersForField.length !== 1) { + return + } + const filter = filtersForField[0] + if (filter.operatorValue === 'contains' && filter.value) { + setSearch(filter.value) initializedFromFilter.current = true } }, [props.searchField, props.filterModel]) From f754718d7ede72e69ae98b35f6a5a56618746512 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Fri, 24 Jul 2026 12:34:53 -0400 Subject: [PATCH 5/7] Fix lint CI failure and add regression tests for search bar - Fix npm audit failure: use --audit-level=high since react-router moderate CVEs have no v6 patch (fix requires v7 + React 18 migration) - Fix negated filter initialization: check filter.not !== true before populating quick-search from a contains filter - Add GridToolbar tests: initialization from filters, negated filter exclusion, Enter/button search triggering, blur not triggering search - Add JobRunsTable requestSearch tests: immutable filter replacement, preservation of unrelated filters Co-Authored-By: Claude Opus 4.6 --- Makefile | 2 +- sippy-ng/src/datagrid/GridToolbar.jsx | 6 +- sippy-ng/src/datagrid/GridToolbar.test.jsx | 147 +++++++++++++++++++++ sippy-ng/src/jobs/JobRunsTable.test.jsx | 92 +++++++++++++ 4 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 sippy-ng/src/datagrid/GridToolbar.test.jsx create mode 100644 sippy-ng/src/jobs/JobRunsTable.test.jsx diff --git a/Makefile b/Makefile index 63f459df15..b94c74f2c3 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ lint: builddir npm cd sippy-ng; npx eslint . # See https://github.com/facebook/create-react-app/issues/11174 about # why we only audit production deps: - cd sippy-ng; npm audit --omit=dev + cd sippy-ng; npm audit --omit=dev --audit-level=high npm: sippy-ng/node_modules/.package-lock.json diff --git a/sippy-ng/src/datagrid/GridToolbar.jsx b/sippy-ng/src/datagrid/GridToolbar.jsx index f96889333f..5cb63c0efa 100644 --- a/sippy-ng/src/datagrid/GridToolbar.jsx +++ b/sippy-ng/src/datagrid/GridToolbar.jsx @@ -57,7 +57,11 @@ export default function GridToolbar(props) { return } const filter = filtersForField[0] - if (filter.operatorValue === 'contains' && filter.value) { + if ( + filter.operatorValue === 'contains' && + filter.not !== true && + filter.value + ) { setSearch(filter.value) initializedFromFilter.current = true } diff --git a/sippy-ng/src/datagrid/GridToolbar.test.jsx b/sippy-ng/src/datagrid/GridToolbar.test.jsx new file mode 100644 index 0000000000..df2256fd87 --- /dev/null +++ b/sippy-ng/src/datagrid/GridToolbar.test.jsx @@ -0,0 +1,147 @@ +import '@testing-library/jest-dom' +import { createTheme, ThemeProvider } from '@mui/material/styles' +import { render, screen } from '@testing-library/react' +import GridToolbar from './GridToolbar' +import React from 'react' +import userEvent from '@testing-library/user-event' + +vi.mock('@mui/x-data-grid', () => ({ + GridToolbarDensitySelector: () => null, +})) + +vi.mock('./GridToolbarFilterMenu', () => ({ + default: () => null, +})) + +const theme = createTheme() + +function renderToolbar(props = {}) { + const defaults = { + doSearch: vi.fn(), + clearSearch: vi.fn(), + setFilterModel: vi.fn(), + addFilters: vi.fn(), + filterModel: { items: [] }, + } + return render( + + + + ) +} + +describe('GridToolbar', () => { + describe('search bar initialization from filter', () => { + it('populates search from a single positive contains filter', () => { + renderToolbar({ + searchField: 'name', + filterModel: { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + value: 'my-job', + }, + ], + }, + }) + expect(screen.getByPlaceholderText('Search…')).toHaveValue('my-job') + }) + + it('does not populate search from a negated contains filter', () => { + renderToolbar({ + searchField: 'name', + filterModel: { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + not: true, + value: 'excluded', + }, + ], + }, + }) + expect(screen.getByPlaceholderText('Search…')).toHaveValue('') + }) + + it('does not populate search when multiple filters exist for the field', () => { + renderToolbar({ + searchField: 'name', + filterModel: { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + value: 'a', + }, + { + columnField: 'name', + operatorValue: 'contains', + value: 'b', + }, + ], + }, + }) + expect(screen.getByPlaceholderText('Search…')).toHaveValue('') + }) + + it('does not populate search when operator is not contains', () => { + renderToolbar({ + searchField: 'name', + filterModel: { + items: [ + { + columnField: 'name', + operatorValue: 'equals', + value: 'exact', + }, + ], + }, + }) + expect(screen.getByPlaceholderText('Search…')).toHaveValue('') + }) + }) + + describe('search interactions', () => { + it('triggers search on Enter key', async () => { + const doSearch = vi.fn() + renderToolbar({ doSearch }) + + const input = screen.getByPlaceholderText('Search…') + await userEvent.type(input, 'test-query{enter}') + expect(doSearch).toHaveBeenCalledWith('test-query') + }) + + it('triggers search on search button click', async () => { + const doSearch = vi.fn() + renderToolbar({ doSearch }) + + const input = screen.getByPlaceholderText('Search…') + await userEvent.type(input, 'btn-query') + await userEvent.click(screen.getByTitle('Search')) + expect(doSearch).toHaveBeenCalledWith('btn-query') + }) + + it('does not trigger search on blur', async () => { + const doSearch = vi.fn() + renderToolbar({ doSearch }) + + const input = screen.getByPlaceholderText('Search…') + await userEvent.type(input, 'blur-query') + await userEvent.tab() + expect(doSearch).not.toHaveBeenCalled() + }) + + it('clears search on clear button click', async () => { + const clearSearch = vi.fn() + renderToolbar({ clearSearch }) + + const input = screen.getByPlaceholderText('Search…') + await userEvent.type(input, 'something') + await userEvent.click(screen.getByTitle('Clear')) + expect(clearSearch).toHaveBeenCalled() + expect(input).toHaveValue('') + }) + }) +}) diff --git a/sippy-ng/src/jobs/JobRunsTable.test.jsx b/sippy-ng/src/jobs/JobRunsTable.test.jsx new file mode 100644 index 0000000000..9ae1d0d675 --- /dev/null +++ b/sippy-ng/src/jobs/JobRunsTable.test.jsx @@ -0,0 +1,92 @@ +import '@testing-library/jest-dom' + +/** + * The requestSearch pattern used in JobRunsTable (and all other table + * components) immutably replaces the search-field filter while preserving + * other filters. We test the pure logic here because the full component + * requires extensive infrastructure (react-router, query-params, DataGrid, + * API). + */ +function requestSearch(filterModel, searchField, searchValue) { + const newItems = filterModel.items.filter( + (f) => f.columnField !== searchField + ) + newItems.push({ + id: 99, + columnField: searchField, + operatorValue: 'contains', + value: searchValue, + }) + return { + ...filterModel, + items: newItems, + } +} + +describe('JobRunsTable requestSearch logic', () => { + it('replaces an existing job filter immutably', () => { + const original = { + items: [ + { id: 1, columnField: 'job', operatorValue: 'contains', value: 'old' }, + ], + } + const result = requestSearch(original, 'job', 'new-query') + expect(result.items).toHaveLength(1) + expect(result.items[0]).toEqual({ + id: 99, + columnField: 'job', + operatorValue: 'contains', + value: 'new-query', + }) + expect(original.items).toHaveLength(1) + expect(original.items[0].value).toBe('old') + }) + + it('preserves filters for other fields', () => { + const original = { + items: [ + { + id: 1, + columnField: 'cluster', + operatorValue: 'contains', + value: 'build01', + }, + { + id: 2, + columnField: 'job', + operatorValue: 'contains', + value: 'old', + }, + ], + } + const result = requestSearch(original, 'job', 'new-query') + expect(result.items).toHaveLength(2) + expect(result.items[0].columnField).toBe('cluster') + expect(result.items[0].value).toBe('build01') + expect(result.items[1].value).toBe('new-query') + }) + + it('adds a filter when none exists for the field', () => { + const original = { items: [] } + const result = requestSearch(original, 'job', 'my-search') + expect(result.items).toHaveLength(1) + expect(result.items[0].value).toBe('my-search') + expect(original.items).toHaveLength(0) + }) + + it('clears the search by setting an empty value', () => { + const original = { + items: [ + { + id: 99, + columnField: 'job', + operatorValue: 'contains', + value: 'prev', + }, + ], + } + const result = requestSearch(original, 'job', '') + expect(result.items).toHaveLength(1) + expect(result.items[0].value).toBe('') + }) +}) From 590709c00d3363fedb540f6d8c80d2b98a8c4783 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Fri, 24 Jul 2026 12:38:01 -0400 Subject: [PATCH 6/7] Revert Makefile audit-level change Another PR is fixing the react-router audit issue directly. Co-Authored-By: Claude Opus 4.6 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b94c74f2c3..63f459df15 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ lint: builddir npm cd sippy-ng; npx eslint . # See https://github.com/facebook/create-react-app/issues/11174 about # why we only audit production deps: - cd sippy-ng; npm audit --omit=dev --audit-level=high + cd sippy-ng; npm audit --omit=dev npm: sippy-ng/node_modules/.package-lock.json From 57228f08c24cf60b064f37fbc23d79fc2a26abf2 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Fri, 24 Jul 2026 14:07:05 -0400 Subject: [PATCH 7/7] Address review: fix immutable state update and columnField mismatch - JobAnalysis.jsx: Convert requestSearch to immutable pattern using spread operator so React detects the state change - ReleasePayloadJobRuns.jsx: Fix columnField from 'releaseTag' to 'release_tag' to match the filter removal and column definition Co-Authored-By: Claude Opus 4.6 --- sippy-ng/src/jobs/JobAnalysis.jsx | 9 +++------ sippy-ng/src/releases/ReleasePayloadJobRuns.jsx | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/sippy-ng/src/jobs/JobAnalysis.jsx b/sippy-ng/src/jobs/JobAnalysis.jsx index bf8002afef..8360116c59 100644 --- a/sippy-ng/src/jobs/JobAnalysis.jsx +++ b/sippy-ng/src/jobs/JobAnalysis.jsx @@ -261,17 +261,14 @@ export function JobAnalysis(props) { } const requestSearch = (searchValue) => { - const currentFilters = testFilter - currentFilters.items = currentFilters.items.filter( - (f) => f.columnField !== 'name' - ) - currentFilters.items.push({ + const newItems = testFilter.items.filter((f) => f.columnField !== 'name') + newItems.push({ id: 99, columnField: 'name', operatorValue: 'contains', value: searchValue, }) - setTestFilter(currentFilters) + setTestFilter({ ...testFilter, items: newItems }) } const updateSelectionModel = (m) => { diff --git a/sippy-ng/src/releases/ReleasePayloadJobRuns.jsx b/sippy-ng/src/releases/ReleasePayloadJobRuns.jsx index 6daaefcaea..3c2791153c 100644 --- a/sippy-ng/src/releases/ReleasePayloadJobRuns.jsx +++ b/sippy-ng/src/releases/ReleasePayloadJobRuns.jsx @@ -160,7 +160,7 @@ function ReleasePayloadJobRuns(props) { ) newItems.push({ id: 99, - columnField: 'releaseTag', + columnField: 'release_tag', operatorValue: 'contains', value: searchValue, })