mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-03 16:10:21 +00:00
fix(queries): read row-click payload as (row, event) in User Summary (#15306)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -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);
|
||||
|
||||
@@ -249,7 +249,7 @@ export default defineComponent({
|
||||
emit("delete:queries");
|
||||
};
|
||||
|
||||
const getAllUserQueries = (event: any, row: any) => {
|
||||
const getAllUserQueries = (row: any) => {
|
||||
emit("filter:queries", row);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user