From c3bfda444d58ff0e44b816e6c519c8d718161aea Mon Sep 17 00:00:00 2001 From: Neha P <144308844+neha00290@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:24:01 +0000 Subject: [PATCH] fix(logs): show the run-query prompt when re-applying from Search History (#16213) Co-authored-by: Shrinath Rao --- web/src/plugins/logs/Index.autoRun.spec.ts | 64 +++++++++++++++++++++- web/src/plugins/logs/Index.vue | 9 ++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/web/src/plugins/logs/Index.autoRun.spec.ts b/web/src/plugins/logs/Index.autoRun.spec.ts index 4b03bb0f52..0586df4561 100644 --- a/web/src/plugins/logs/Index.autoRun.spec.ts +++ b/web/src/plugins/logs/Index.autoRun.spec.ts @@ -243,8 +243,12 @@ describe("Logs Index — first landing and URL loads (item 2)", { timeout: 30000 }); describe("re-applied query links (AI chat, search history)", () => { - const reApply = async (query: Record) => { - store.state.zoConfig = { ...originalZoConfig, auto_query_enabled: true }; + const reApply = async ( + query: Record, + zoConfig: Record = {}, + logs?: Mock, + ) => { + store.state.zoConfig = { ...originalZoConfig, auto_query_enabled: true, ...zoConfig }; (store.state as any).logs = { isInitialized: false, logs: {} }; vi.spyOn(store, "dispatch").mockResolvedValue(undefined as any); (getFieldsFromQuery as Mock).mockResolvedValue({ fields: [], filters: [], streamName: "" }); @@ -253,6 +257,7 @@ describe("Logs Index — first landing and URL loads (item 2)", { timeout: 30000 attachTo: "#app", global: { provide: { store }, plugins: [i18n, router] }, }); + if (logs) wrapper.vm.autoRun.setExecutors({ logs }); await vi.waitFor( () => expect(wrapper.vm.searchObj.data.stream.streamLists.length).toBeGreaterThan(0), { timeout: 10000 }, @@ -283,6 +288,61 @@ describe("Logs Index — first landing and URL loads (item 2)", { timeout: 30000 }, ); + it.each(["ai_chat_query", "search_history_re_apply"])( + "%s loaded without running shows the run-query prompt, not an empty result grid", + async (type) => { + await reApply( + { type, stream: "k8s_json", stream_type: "logs", period: "15m" }, + { auto_query_enabled: false, query_on_stream_selection: true }, + ); + await vi.waitFor(() => + expect(wrapper.vm.searchObj.data.stream.selectedStreamFields.length).toBeGreaterThan(0), + ); + await flushPromises(); + expect(wrapper.vm.searchObj.meta.searchApplied).toBe(false); + expect(wrapper.find('[data-test="logs-search-apply-search-text"]').exists()).toBe(true); + expect(wrapper.find('[data-test="logs-search-search-result"]').exists()).toBe(false); + }, + ); + + it("a search Auto Run starts during a re-apply keeps its hits", async () => { + const logs = vi.fn(async () => { + wrapper.vm.searchObj.data.queryResults = { hits: [{ _timestamp: 1, body: "row" }] }; + wrapper.vm.searchObj.meta.searchApplied = true; + }); + await reApply( + { type: "search_history_re_apply", stream: "k8s_json", stream_type: "logs", period: "15m" }, + { auto_query_enabled: true, query_on_stream_selection: false }, + logs, + ); + await vi.waitFor(() => + expect(wrapper.vm.searchObj.data.stream.selectedStreamFields.length).toBeGreaterThan(0), + ); + await vi.waitFor(() => expect(logs).toHaveBeenCalled()); + await flushPromises(); + expect(wrapper.vm.searchObj.data.queryResults.hits).toHaveLength(1); + expect(wrapper.find('[data-test="logs-search-apply-search-text"]').exists()).toBe(false); + }); + + it("a search Auto Run starts during a re-apply keeps loading, not the run-query prompt, while it is pending", async () => { + const logs = vi.fn(() => { + wrapper.vm.searchObj.loading = true; + return new Promise(() => {}); + }); + await reApply( + { type: "search_history_re_apply", stream: "k8s_json", stream_type: "logs", period: "15m" }, + { auto_query_enabled: true, query_on_stream_selection: false }, + logs, + ); + await vi.waitFor(() => + expect(wrapper.vm.searchObj.data.stream.selectedStreamFields.length).toBeGreaterThan(0), + ); + await vi.waitFor(() => expect(logs).toHaveBeenCalled()); + await flushPromises(); + expect(wrapper.vm.searchObj.loading).toBe(true); + expect(wrapper.find('[data-test="logs-search-apply-search-text"]').exists()).toBe(false); + }); + it("a re-applied query loaded without running lets the user's edited run push history", async () => { await router.replace({ name: "home" }); await reApply({ diff --git a/web/src/plugins/logs/Index.vue b/web/src/plugins/logs/Index.vue index b90b8de0a8..f0c3363450 100644 --- a/web/src/plugins/logs/Index.vue +++ b/web/src/plugins/logs/Index.vue @@ -1748,12 +1748,19 @@ export default defineComponent({ // loadLogsData() minus getQueryData(): a re-applied query is loaded for the user to run, not run for them. async function applyReAppliedQuery() { resetRunStateForReapply(); + const gridAtStart = autoRun.engine.currentGeneration("grid")?.id; searchObj.meta.searchApplied = false; + // The run-query prompt needs a `hits` array; set before the awaits so an Auto Run search that lands meanwhile keeps its hits. + searchObj.data.queryResults.hits = []; await getStreamList(true, { fromUrl: true }); await getFunctions(); await extractFields(); refreshData(); - searchObj.loading = false; + const latest = autoRun.engine.currentGeneration("grid"); + // A search started during the load owns the loading state; its completion or cancel clears it. + if (!latest || latest.id === gridAtStart || latest.settled || latest.cancelled) { + searchObj.loading = false; + } } // Helper function for handling the stream explorer