fix(queries): read row-click payload as (row, event) in User Summary (#15306)

This commit is contained in:
Bhargav
2026-10-13 15:34:05 +00:00
committed by GitHub
parent 6368bc212e
commit 6c0a27a7fa
5 changed files with 226 additions and 4 deletions
@@ -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();
}
}
@@ -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();
}
});
});
@@ -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 <http://www.gnu.org/licenses/>.
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);
});
});
@@ -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);
+1 -1
View File
@@ -249,7 +249,7 @@ export default defineComponent({
emit("delete:queries");
};
const getAllUserQueries = (event: any, row: any) => {
const getAllUserQueries = (row: any) => {
emit("filter:queries", row);
};