diff --git a/changelog/unreleased/issue-25028.toml b/changelog/unreleased/issue-25028.toml new file mode 100644 index 0000000000..a4fa611e80 --- /dev/null +++ b/changelog/unreleased/issue-25028.toml @@ -0,0 +1,5 @@ +type = "fixed" +message = "Fix saved search URL query overrides to execute with URL values on initial search execution." + +issues = ["25028"] +pulls = ["25051"] diff --git a/graylog2-web-interface/src/views/components/SynchronizeUrl.tsx b/graylog2-web-interface/src/views/components/SynchronizeUrl.tsx index 4d82456a6a..7afac1a217 100644 --- a/graylog2-web-interface/src/views/components/SynchronizeUrl.tsx +++ b/graylog2-web-interface/src/views/components/SynchronizeUrl.tsx @@ -25,7 +25,7 @@ import { selectView } from 'views/logic/slices/viewSelectors'; import { selectSearchExecutionState } from 'views/logic/slices/searchExecutionSelectors'; import useLocation from 'routing/useLocation'; import useQuery from 'routing/useQuery'; -import { updateView } from 'views/logic/slices/viewSlice'; +import { updateView, executeActiveQuery } from 'views/logic/slices/viewSlice'; const bindSearchParamsFromQueryThunk = (query: { [key: string]: unknown }) => async (dispatch: ViewsDispatch, getState: () => RootState) => { @@ -41,7 +41,10 @@ const bindSearchParamsFromQueryThunk = const [newView] = result; if (newView !== view) { - return dispatch(updateView(newView, true)); + // Update view, but don't create new search because this already happened in bindSearchParamsFromQuery + await dispatch(updateView(newView)); + + return dispatch(executeActiveQuery()); } }; diff --git a/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.test.ts b/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.test.ts index a19a37f7d1..168d577198 100644 --- a/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.test.ts +++ b/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.test.ts @@ -73,6 +73,27 @@ describe('BindSearchParamsFromQuery should', () => { const [newView] = await bindSearchParamsFromQuery(input); expect(findMockQuery(newView).query.query_string).toBe('gl2_source_input:source-input-id'); + expect(newView.search.id).not.toBe(view.search.id); + }); + + it('apply URL query override without mutating the original saved query', async () => { + const savedQuery = Query.builder() + .id(MOCK_VIEW_QUERY_ID) + .query(createElasticsearchQueryString('persisted:query')) + .build(); + const savedSearch = Search.create().toBuilder().queries([savedQuery]).build(); + const savedView = view.toBuilder().search(savedSearch).build(); + const input = { + ...defaultInput, + view: savedView, + query: { q: 'override:query' }, + }; + + const [newView] = await bindSearchParamsFromQuery(input); + + expect(findMockQuery(newView).query.query_string).toBe('override:query'); + expect(findMockQuery(savedView).query.query_string).toBe('persisted:query'); + expect(newView.search.id).not.toBe(savedView.search.id); }); it('not update query string when no query param is provided', async () => { diff --git a/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.ts b/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.ts index 7797b71960..75f9320b65 100644 --- a/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.ts +++ b/graylog2-web-interface/src/views/hooks/BindSearchParamsFromQuery.ts @@ -20,6 +20,7 @@ import isDeepEqual from 'stores/isDeepEqual'; import type { ViewHook, ViewHookArguments } from 'views/logic/hooks/ViewHook'; import View from 'views/logic/views/View'; import normalizeSearchURLQueryParams from 'views/logic/NormalizeSearchURLQueryParams'; +import createSearch from 'views/logic/slices/createSearch'; const bindSearchParamsFromQuery: ViewHook = async ({ query, view, executionState }: ViewHookArguments) => { if (view.type !== View.Type.Search) { @@ -67,7 +68,7 @@ const bindSearchParamsFromQuery: ViewHook = async ({ query, view, executionState return [view, executionState]; } - const newSearch = view.search.toBuilder().queries([newQuery]).build(); + const newSearch = await createSearch(view.search.toBuilder().newId().queries([newQuery]).build()); const newView = view.toBuilder().search(newSearch).build(); return [newView, executionState]; diff --git a/graylog2-web-interface/src/views/hooks/useSyncWithQueryParameters.test.ts b/graylog2-web-interface/src/views/hooks/useSyncWithQueryParameters.test.ts index 569f2ef69a..d1efe4b604 100644 --- a/graylog2-web-interface/src/views/hooks/useSyncWithQueryParameters.test.ts +++ b/graylog2-web-interface/src/views/hooks/useSyncWithQueryParameters.test.ts @@ -87,6 +87,14 @@ describe('SyncWithQueryParameters', () => { ); }); + it('preserves a saved-search URL query override and appends missing timerange params', () => { + asMock(useCurrentQuery).mockReturnValue(createQuery(lastFiveMinutes, [], 'new-query')); + renderHook(() => useSyncWithQueryParameters('/search/example-search-id?q=new-query')); + + expect(history.replace).toHaveBeenCalledWith('/search/example-search-id?q=new-query&rangetype=relative&from=300'); + expect(history.push).not.toHaveBeenCalled(); + }); + it('if time range is relative with from and to', () => { asMock(useCurrentQuery).mockReturnValue(createQuery({ ...lastFiveMinutes, to: 240 })); renderHook(() => useSyncWithQueryParameters('/search'));