From 6c0a27a7fa54d262e9d3f7a607b3e008fa06c03c Mon Sep 17 00:00:00 2001 From: Bhargav Date: Tue, 13 Oct 2026 15:34:05 +0000 Subject: [PATCH] fix(queries): read row-click payload as (row, event) in User Summary (#15306) --- .../pages/generalPages/queryManagementPage.js | 35 +++++ ...settings-query-management-redirect.spec.js | 41 +++++ .../RunningQueriesDrillThrough.spec.ts | 147 ++++++++++++++++++ .../components/queries/SummaryList.spec.ts | 5 +- web/src/components/queries/SummaryList.vue | 2 +- 5 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 web/src/components/queries/RunningQueriesDrillThrough.spec.ts diff --git a/tests/ui-testing/pages/generalPages/queryManagementPage.js b/tests/ui-testing/pages/generalPages/queryManagementPage.js index 753df9f23e..0e8e6b6876 100644 --- a/tests/ui-testing/pages/generalPages/queryManagementPage.js +++ b/tests/ui-testing/pages/generalPages/queryManagementPage.js @@ -22,6 +22,10 @@ export class QueryManagementPage { // `visible !== false` items), so count 0 rather than merely hidden. this.queryManagementTab = page.locator('[data-test="query-management-tab"]'); this.enterpriseLockedPage = page.locator('[data-test="enterprise-feature-locked"]'); + this.queryTypeTabs = page.locator('[data-test="running-queries-query-type-tabs"]'); + this.summaryList = page.locator('[data-test="running-queries-summary-list"]'); + this.allQueriesList = page.locator('[data-test="running-queries-all-queries-list"]'); + this.searchTypeFilter = this.allQueriesList.locator('[data-otoggle-value="dashboards"]'); } baseUrl() { @@ -54,4 +58,35 @@ export class QueryManagementPage { await expect(this.enterpriseLockedPage).toBeVisible({ timeout: 15000 }); await expect(this.page).toHaveURL(/enterprise-locked\?feature=queryManagement/); } + + async stubRunningQueries(org, queries) { + await this.page.route(`**/api/${org}/query_manager/status`, (route) => + route.fulfill({ json: { status: queries } }) + ); + } + + async navigateToRunningQueries(org) { + await this.navigateToQueryManagement(org); + await expect(this.queryTypeTabs).toBeVisible({ timeout: 15000 }); + } + + queryTypeTab(value) { + return this.queryTypeTabs.locator(`[data-otoggle-value="${value}"]`); + } + + async selectQueryTypeTab(value) { + await this.queryTypeTab(value).click(); + } + + async drillIntoUser(email) { + await this.summaryList.getByText(email, { exact: true }).first().click(); + } + + async expectAllQueriesViewFor(email) { + await expect(this.queryTypeTab('all')).toHaveAttribute('data-state', 'on'); + await expect(this.allQueriesList).toBeVisible(); + await expect(this.summaryList).toBeHidden(); + await expect(this.searchTypeFilter).toBeVisible(); + await expect(this.allQueriesList.getByText(email).first()).toBeVisible(); + } } diff --git a/tests/ui-testing/playwright-tests/Management/settings-query-management-redirect.spec.js b/tests/ui-testing/playwright-tests/Management/settings-query-management-redirect.spec.js index 8576a955c7..f2d03175ff 100644 --- a/tests/ui-testing/playwright-tests/Management/settings-query-management-redirect.spec.js +++ b/tests/ui-testing/playwright-tests/Management/settings-query-management-redirect.spec.js @@ -61,4 +61,45 @@ test.describe("Settings Query Management Redirect testcases", () => { await pm.queryManagementPage.navigateToQueryManagement(orgId); await pm.queryManagementPage.expectGeneralPageTitleVisible(); }); + + test("drills into a user's queries from User Summary on every click, not only the first (enterprise)", { + tag: ['@query-management-drill-through', '@all', '@enterprise', '@P2'] + }, async ({ page }) => { + const edition = await pm.editionFeaturesPage.detectEdition(); + test.skip(edition !== 'enterprise', `Runs only on Enterprise build (detected: ${edition})`); + + // o2-enterprise#2876: the second drill-through left the User Summary table showing. + const email = 'drill.user@example.com'; + const now = Date.now() * 1000; + const runningQuery = (traceId, userId) => ({ + trace_id: traceId, + user_id: userId, + org_id: '_meta', + search_type: 'dashboards', + created_at: now - 5000000, + started_at: now - 5000000, + status: 'running', + work_group: 'default', + stream_type: 'logs', + query: { start_time: now - 60000000, end_time: now, sql: 'SELECT 1' }, + scan_stats: { files: 1, records: 1, original_size: 1, compressed_size: 1 }, + search_event_context: { folder_name: 'f', dashboard_name: 'd' }, + }); + await pm.queryManagementPage.stubRunningQueries('_meta', [ + runningQuery('trace-a', email), + runningQuery('trace-b', email), + runningQuery('trace-c', 'other.user@example.com'), + ]); + + await pm.queryManagementPage.navigateToRunningQueries('_meta'); + + for (const round of [1, 2, 3]) { + testLogger.info(`Drill-through round ${round}`); + await pm.queryManagementPage.drillIntoUser(email); + await pm.queryManagementPage.expectAllQueriesViewFor(email); + + await pm.queryManagementPage.selectQueryTypeTab('summary'); + await expect(pm.queryManagementPage.summaryList).toBeVisible(); + } + }); }); diff --git a/web/src/components/queries/RunningQueriesDrillThrough.spec.ts b/web/src/components/queries/RunningQueriesDrillThrough.spec.ts new file mode 100644 index 0000000000..66b2fdca9d --- /dev/null +++ b/web/src/components/queries/RunningQueriesDrillThrough.spec.ts @@ -0,0 +1,147 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { mount, flushPromises } from "@vue/test-utils"; +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; +import RunningQueries from "@/components/queries/RunningQueries.vue"; +import i18n from "@/locales"; +import store from "@/test/unit/helpers/store"; +import SearchService from "@/services/search"; + +vi.mock("@/services/search", async (importOriginal) => { + const { overlayServiceMock } = await import("@/test/unit/helpers/mockService"); + return overlayServiceMock(await importOriginal(), { + default: { + get_running_queries: vi.fn(), + delete_running_queries: vi.fn(), + }, + }); +}); + +vi.mock("@/composables/useIsMetaOrg", () => ({ + default: () => ({ isMetaOrg: true }), +})); + +const node = document.createElement("div"); +node.setAttribute("id", "app"); +document.body.appendChild(node); + +const query = (traceId: string, userId: string, searchType: string) => ({ + trace_id: traceId, + user_id: userId, + org_id: "org-1", + search_type: searchType, + created_at: 1640995200000000, + started_at: 1640995200000000, + status: "running", + work_group: "default", + stream_type: "logs", + query: { start_time: 1640995200000000, end_time: 1640995260000000, sql: "SELECT 1" }, + scan_stats: { files: 1, records: 1, original_size: 1, compressed_size: 1 }, + search_event_context: { dashboard_name: "d", folder_name: "f" }, +}); + +describe("RunningQueries drill-through from User Summary", () => { + let wrapper: any = null; + + beforeEach(async () => { + store.state.zoConfig = { ...store.state.zoConfig, meta_org: "test-org" }; + vi.clearAllMocks(); + (SearchService.get_running_queries as any).mockResolvedValue({ + data: { + status: [ + query("t1", "alice@example.com", "dashboards"), + query("t2", "alice@example.com", "dashboards"), + query("t3", "bob@example.com", "ui"), + ], + }, + }); + + wrapper = mount(RunningQueries, { + attachTo: "#app", + global: { + plugins: [i18n], + provide: { store }, + mocks: { $store: store }, + stubs: { OIcon: true, "confirm-dialog": true }, + }, + }); + await flushPromises(); + }); + + afterEach(() => wrapper?.unmount()); + + const visible = (testId: string) => { + const el = wrapper.find(`[data-test="${testId}"]`).element as HTMLElement; + return el.style.display !== "none"; + }; + + const tab = (value: string) => + wrapper + .find('[data-test="running-queries-query-type-tabs"]') + .findAll("button") + .find((b: any) => b.attributes("data-otoggle-value") === value); + + const drill = async (userId: string) => { + const summary = wrapper.findComponent({ + name: "RunningQueriesList", + props: { searchType: undefined }, + }); + void summary; + wrapper.vm.filterUserQueries({ user_id: userId, search_type_label: "dashboards" }); + await flushPromises(); + }; + + it("shows the All Queries list on every drill-through, not only the first", async () => { + for (const round of [1, 2, 3]) { + await drill("alice@example.com"); + expect(visible("running-queries-all-queries-list"), `round ${round} all`).toBe(true); + expect(visible("running-queries-summary-list"), `round ${round} summary`).toBe(false); + expect(tab("all")?.attributes("data-state")).toBe("on"); + + await tab("summary")!.trigger("click"); + await flushPromises(); + expect(visible("running-queries-summary-list"), `round ${round} back`).toBe(true); + expect(visible("running-queries-all-queries-list"), `round ${round} back all`).toBe(false); + } + }); + + it("drills in via a real row click, repeatedly", async () => { + for (const round of [1, 2, 3]) { + const rows = wrapper.find('[data-test="running-queries-summary-list"]').findAll("tbody tr"); + expect(rows.length, `round ${round} rows`).toBeGreaterThan(0); + await rows[0].trigger("click"); + await flushPromises(); + expect(wrapper.vm.selectedQueryTypeTab, `round ${round}`).toBe("all"); + expect(visible("running-queries-all-queries-list")).toBe(true); + expect(visible("running-queries-summary-list")).toBe(false); + await tab("summary")!.trigger("click"); + await flushPromises(); + expect(wrapper.vm.selectedQueryTypeTab).toBe("summary"); + } + }); + + it("keeps the tab highlight and rendered list in agreement after a repeat drill", async () => { + await drill("alice@example.com"); + await tab("summary")!.trigger("click"); + await flushPromises(); + await drill("alice@example.com"); + + expect(wrapper.vm.selectedQueryTypeTab).toBe("all"); + expect(tab("all")?.attributes("data-state")).toBe("on"); + expect(wrapper.vm.rowsQuery.every((r: any) => r.user_id === "alice@example.com")).toBe(true); + expect(wrapper.vm.rowsQuery).toHaveLength(2); + }); +}); diff --git a/web/src/components/queries/SummaryList.spec.ts b/web/src/components/queries/SummaryList.spec.ts index 595bb06ee6..c85ec32faf 100644 --- a/web/src/components/queries/SummaryList.spec.ts +++ b/web/src/components/queries/SummaryList.spec.ts @@ -193,10 +193,9 @@ describe("SummaryList.vue", () => { // Test 17: Test getAllUserQueries function it("should emit filter:queries when getAllUserQueries is called", () => { - const mockEvent = { type: "click" }; const mockRow = { user_id: "test@example.com" }; - wrapper.vm.getAllUserQueries(mockEvent, mockRow); + wrapper.vm.getAllUserQueries(mockRow); expect(wrapper.emitted("filter:queries")).toBeTruthy(); expect(wrapper.emitted("filter:queries")[0]).toEqual([mockRow]); @@ -327,7 +326,7 @@ describe("SummaryList.vue", () => { it("should handle multiple emit events correctly", () => { // Emit multiple events wrapper.vm.handleMultiQueryCancel(); - wrapper.vm.getAllUserQueries({}, mockRows[0]); + wrapper.vm.getAllUserQueries(mockRows[0]); wrapper.vm.confirmDeleteAction({ row: mockRows[0] }); expect(wrapper.emitted("delete:queries")).toHaveLength(2); diff --git a/web/src/components/queries/SummaryList.vue b/web/src/components/queries/SummaryList.vue index 863c02dd7a..b20764f3ed 100644 --- a/web/src/components/queries/SummaryList.vue +++ b/web/src/components/queries/SummaryList.vue @@ -249,7 +249,7 @@ export default defineComponent({ emit("delete:queries"); }; - const getAllUserQueries = (event: any, row: any) => { + const getAllUserQueries = (row: any) => { emit("filter:queries", row); };